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 (leads, 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 Lead 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 Lead feature is organized under src/features/lead/ with the following structure:
Feature Interfaces
The lead interfaces define the data models and Zod validation schemas:
// src/features/lead/lead.interface.ts
// Feature interfaces define data models and validation schemas
export interface Lead {
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 LeadCreationSchema = 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 LeadProcedure encapsulates all data operations and business rules:
// src/features/lead/procedures/lead.procedure.ts
export const LeadProcedure = igniter.procedure({
name: 'LeadProcedure',
handler: (_, { context }) => {
return {
lead: {
findMany: async (organizationId: string): Promise<Lead[]> => {
return context.services.database.lead.findMany({
where: { organizationId },
})
},
create: async (organizationId: string, data: CreateLeadBody): Promise<Lead> => {
const lead = await context.services.database.lead.create({
data: { ...data, organizationId },
})
// Business logic: Trigger notifications for new leads
await context.services.notification.send({
type: 'LEAD_CREATED',
context: { organizationId },
data: { leadName: lead.name, leadEmail: lead.email },
})
return lead
},
},
}
},
})Feature Controller
The LeadController exposes RESTful API endpoints:
// src/features/lead/controllers/lead.controller.ts
export const LeadController = igniter.controller({
name: 'Lead',
path: '/leads',
description: 'Manage customer leads.',
actions: {
list: igniter.query({
name: 'List',
description: 'List all leads for an organization.',
path: '/',
use: [AuthFeatureProcedure(), LeadProcedure()],
query: LeadQuerySchema,
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 leads = await context.lead.findMany(session.organization.id)
return response.success(leads)
},
}),
create: igniter.mutation({
name: 'Create',
description: 'Create a new lead.',
path: '/',
method: 'POST',
use: [AuthFeatureProcedure(), LeadProcedure(), IntegrationFeatureProcedure()],
body: LeadCreationSchema,
handler: async ({ context, request, response }) => {
const session = await context.auth.getSession({
requirements: 'authenticated',
roles: ['admin', 'owner', 'member'],
})
const lead = await context.lead.create(session.organization!.id, request.body)
return response.created(lead)
},
}),
},
})Controller Registration
The LeadController 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
lead: LeadController,
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 Lead endpoints through the interactive API documentation at http://localhost:3000/api/v1/docs.