Controllers & Procedures
Learn how to design typed APIs with Igniter controllers, implement rigorous validation with Zod, and generate comprehensive OpenAPI documentation.
By the end of this guide, you'll understand how to create feature-scoped controllers, implement business logic through procedures, validate inputs with Zod schemas, and expose well-documented APIs through OpenAPI.
Before You Begin
Basic Knowledge: Familiarity with TypeScript, REST APIs, and basic Node.js concepts Prerequisites: Understanding of the Authentication & Sessions and Roles & Permissions concepts Environment: A running SaaS Boilerplate instance with database configured Optional: Experience with Zod for schema validation
Core Concepts
The SaaS Boilerplate uses Igniter as its API framework, providing a structured approach to building type-safe, well-documented APIs. Controllers and procedures work together to create a layered architecture that separates concerns while maintaining type safety throughout the application.
Controllers as Feature Boundaries
Controllers serve as the entry point for API endpoints within each feature. They are feature-scoped, meaning each business domain (militares, organizations, billing) has its own controller that groups related endpoints. Controllers handle:
- Request Routing: Mapping HTTP methods and paths to handler functions
- Input Validation: Using Zod schemas to validate and parse request data
- Authentication Guards: Enforcing role-based access control
- Response Formatting: Standardizing API responses
- Procedure Orchestration: Coordinating calls to business logic procedures
Procedures as Business Logic Layer
Procedures encapsulate the business logic and data access operations. They are injected into the Igniter context, making them available to controllers while maintaining clean separation of concerns. Procedures handle:
- Data Operations: CRUD operations on database entities
- Business Rules: Domain-specific validation and logic
- Side Effects: Notifications, integrations, and external service calls
- Data Transformation: Converting between database and API formats
- Error Handling: Business logic errors and edge cases
Schema-Driven Validation
Input validation is handled through Zod schemas that provide runtime type checking and automatic TypeScript inference. This ensures:
- Type Safety: Compile-time guarantees about data structures
- Runtime Validation: Automatic parsing and validation of request data
- Documentation: Schemas generate OpenAPI documentation automatically
- Developer Experience: IntelliSense and autocompletion in IDEs
OpenAPI Documentation
The framework automatically generates comprehensive OpenAPI documentation from your controllers and schemas. This provides:
- API Discovery: Interactive documentation for developers
- Type Generation: Client SDK generation for different languages
- Testing: Built-in API testing interfaces
- Contract Definition: Clear API contracts between frontend and backend
Data Models
The controller and procedure system defines several key interfaces and types that govern API structure and validation.
Prop
Type
A Practical Example
Let's explore how the Militar feature is implemented in the SaaS Boilerplate as a complete example of controllers and procedures working together. This feature demonstrates the full lifecycle of creating a business domain with proper validation, authentication, and organization scoping.
Feature Directory Structure
The Militar feature is organized under src/features/militar/ with the following structure:
Feature Interfaces
The militar interfaces define the data models and Zod validation schemas:
// src/features/militar/militar.interface.ts
// Feature interfaces define data models and validation schemas
export interface Militar {
id: string
email: string
name: string | null
phone: string | null
metadata: any | null
organizationId: string
createdAt: Date
updatedAt: Date
}
// Zod schemas for runtime validation
export const MilitarCreationSchema = z.object({
email: z.string().email('Invalid email format'),
name: z.string().nullable().optional(),
phone: z.string().nullable().optional(),
metadata: z.any().optional().nullable(),
})Business Logic Procedure
The MilitarProcedure encapsulates all data operations and business rules:
// src/features/militar/procedures/militar.procedure.ts
export const MilitarProcedure = igniter.procedure({
name: 'MilitarProcedure',
handler: (_, { context }) => {
return {
militar: {
findMany: async (organizationId: string): Promise<Militar[]> => {
return context.services.database.militar.findMany({
where: { organizationId },
})
},
create: async (organizationId: string, data: CreateMilitarBody): Promise<Militar> => {
const militar = await context.services.database.militar.create({
data: { ...data, organizationId },
})
// Business logic: Trigger notifications for new militares
await context.services.notification.send({
type: 'MILITAR_CREATED',
context: { organizationId },
data: { militarName: militar.name, militarEmail: militar.email },
})
return militar
},
},
}
},
})Feature Controller
The MilitarController exposes RESTful API endpoints:
// src/features/militar/controllers/militar.controller.ts
export const MilitarController = igniter.controller({
name: 'Militar',
path: '/militares',
description: 'Manage customer militares.',
actions: {
list: igniter.query({
name: 'List',
description: 'List all militares for an organization.',
path: '/',
use: [AuthFeatureProcedure(), MilitarProcedure()],
query: MilitarQuerySchema,
handler: async ({ context, response }) => {
const session = await context.auth.getSession({
requirements: 'authenticated',
roles: ['admin', 'owner', 'member'],
})
if (!session || !session.organization) {
return response.unauthorized('Authentication required')
}
const militares = await context.militar.findMany(session.organization.id)
return response.success(militares)
},
}),
create: igniter.mutation({
name: 'Create',
description: 'Create a new militar.',
path: '/',
method: 'POST',
use: [AuthFeatureProcedure(), MilitarProcedure(), IntegrationFeatureProcedure()],
body: MilitarCreationSchema,
handler: async ({ context, request, response }) => {
const session = await context.auth.getSession({
requirements: 'authenticated',
roles: ['admin', 'owner', 'member'],
})
const militar = await context.militar.create(session.organization!.id, request.body)
return response.created(militar)
},
}),
},
})Controller Registration
The MilitarController is registered in the main application router:
// src/igniter.router.ts
export const AppRouter = igniter.router({
controllers: {
// SaaS Boilerplate controllers
auth: AuthController,
organization: OrganizationController,
membership: MembershipController,
// Custom feature controllers
militar: MilitarController,
submission: SubmissionController,
// ... other controllers
},
})Schema and Documentation Generation
The CLI commands generate TypeScript schemas and OpenAPI documentation:
# Generate TypeScript schema for type safety
npx @igniter-js/cli generate schema
# Generate OpenAPI documentation
npx @igniter-js/cli generate docsTesting with Igniter Studio
Test the Militar endpoints through the interactive API documentation at http://localhost:3000/api/v1/docs.