Controllers & Actions: Building Your API Logic
At the core of every Igniter.js application are Controllers and Actions. This is where you define your API's endpoints, implement your business logic, and handle interactions with your data and services.
Controllers organize related API endpoints, while Actions define individual endpoints with their business logic. Together, they provide a clean, maintainable, and scalable way to build your API.
Controllers
Organizational units that group related Actions together with a shared base path and configuration.
Actions
Individual API endpoints that handle specific requests like fetching data or creating resources.
1. Controllers: Organizing Your Endpoints
A Controller is created using the igniter.controller()
factory function. Its primary role is to define a base path
that acts as a prefix for all the Actions it contains.
Controller Anatomy
Every controller follows the IgniterControllerConfig
interface:
Property | Type | Required | Description |
---|---|---|---|
name | string | ❌ | Optional controller name for documentation and API introspection |
path | string | ✅ | Base URL path prefix for all actions in this controller |
description | string | ❌ | Optional description for documentation generation and OpenAPI spec |
actions | Record<string, Action> | ✅ | Collection of actions where keys are action names and values are action definitions |
Controllers are fully type-safe. The actions
object is validated at compile-time to ensure all actions conform to the proper structure.
Complete Controller Example
// src/features/user/controllers/user.controller.ts
import { igniter } from '@/igniter';
import { z } from 'zod';
export const userController = igniter.controller({
/**
* Optional name for the controller
* Useful for documentation generation and MCP Server tool conversion
*/
name: 'UserController',
/**
* The base path for all actions in this controller.
* All action paths will be prefixed with `/users`.
*/
path: '/users',
/**
* Optional description for documentation generation
* Essential for OpenAPI spec generation and MCP Server AI agent integration
*/
description: 'Handles all user-related operations including CRUD and authentication',
/**
* A collection of all API endpoints related to users.
* Each key becomes an action name, each value defines the endpoint.
*/
actions: {
list: igniter.query({ /* ... */ }),
getById: igniter.query({ /* ... */ }),
create: igniter.mutation({ /* ... */ }),
update: igniter.mutation({ /* ... */ }),
delete: igniter.mutation({ /* ... */ }),
},
});
Controller Properties
Property | Type | Required | Description |
---|---|---|---|
name | string | ❌ | Optional controller name for documentation and API introspection |
path | string | ✅ | Base URL path that prefixes all action paths |
description | string | ❌ | Optional description for documentation, OpenAPI spec, and MCP Server integration |
actions | Record<string, Action> | ✅ | Object containing all actions, where keys are action names |
The final URL for any action is: {controller.path}{action.path}
. For example, if a controller has path: '/users'
and an action has path: '/:id'
, the final URL will be /users/:id
.
2. Actions: The Heart of Your Business Logic
Actions are where the actual work happens. Each Action represents a single API endpoint and is created using either igniter.query()
for read operations or igniter.mutation()
for write operations.
Action Types
Query Actions
Use igniter.query()
for read operations that don't modify data. Typically GET requests.
Mutation Actions
Use igniter.mutation()
for write operations that modify data. POST, PUT, PATCH, DELETE requests.
Action Anatomy
Every action follows the IgniterAction
interface with these core properties:
Property | Type | Required | Query | Mutation | Description |
---|---|---|---|---|---|
name | string | ❌ | ✅ | ✅ | Optional action name for documentation and MCP Server tool conversion |
type | 'query' | 'mutation' | ✅ | ✅ | ✅ | Action type (automatically inferred from creation method) |
path | string | ✅ | ✅ | ✅ | URL path relative to controller, supports parameters like /:id |
method | HTTPMethod | ❌ | ❌ | ✅ | HTTP method (defaults to GET for queries, required for mutations) |
handler | Function | ✅ | ✅ | ✅ | Async function containing your business logic |
query | StandardSchemaV1 | ❌ | ✅ | ✅ | Schema for validating URL query parameters |
body | StandardSchemaV1 | ❌ | ❌ | ✅ | Schema for validating request body data |
use | IgniterProcedure[] | ❌ | ✅ | ✅ | Array of procedure middleware to run before handler |
description | string | ❌ | ✅ | ✅ | Documentation description for OpenAPI docs and MCP Server |
tags | string[] | ❌ | ✅ | ✅ | Tags for categorization and documentation |
$Infer | TActionInfer | ✅ | ✅ | ✅ | Internal type inference helper (automatically managed) |
Complete Action Examples
Query Action with Full Configuration
const getUserById = igniter.query({
/**
* Optional action name for documentation and MCP Server integration
*/
name: 'getUserById',
/**
* URL path - will be combined with controller path
* Final URL: /users/:id (if controller path is '/users')
*/
path: '/:id',
/**
* HTTP method for this endpoint
*/
method: 'GET',
/**
* Query parameters validation using Zod
*/
query: z.object({
include: z.array(z.enum(['posts', 'profile'])).optional(),
fields: z.string().optional(),
}),
/**
* Optional description for API documentation
*/
description: 'Retrieve a specific user by their ID with optional related data',
/**
* Tags for categorization and documentation
*/
tags: ['users', 'read'],
/**
* Middleware stack - runs before the handler
*/
use: [authMiddleware, rateLimitMiddleware],
/**
* The main business logic handler
*/
handler: async (ctx) => {
const { id } = ctx.params; // URL parameters
const { include, fields } = ctx.query; // Validated query params
const user = await getUserFromDatabase(id, {
include,
fields: fields?.split(','),
});
if (!user) {
throw new Error('User not found');
}
return { user };
},
});
Mutation Action with Body Validation
const createUser = igniter.mutation({
/**
* Optional action name for documentation and MCP Server tool conversion
*/
name: 'createUser',
/**
* URL path for creating users
*/
path: '/',
/**
* HTTP method for creation
*/
method: 'POST',
/**
* Request body validation schema
*/
body: z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().int().min(18).optional(),
preferences: z.object({
newsletter: z.boolean().default(false),
theme: z.enum(['light', 'dark']).default('light'),
}).optional(),
}),
/**
* Documentation and metadata
*/
description: 'Create a new user account with validated data',
tags: ['users', 'create'],
/**
* Middleware for authentication and validation
*/
use: [authMiddleware, validatePermissions('user:create')],
/**
* Handler with full type safety
*/
handler: async (ctx) => {
// ctx.body is fully typed based on the schema above
const userData = ctx.body;
// Check if user already exists
const existingUser = await findUserByEmail(userData.email);
if (existingUser) {
throw new Error('User with this email already exists');
}
// Create the user
const newUser = await createUserInDatabase({
...userData,
createdAt: new Date(),
updatedAt: new Date(),
});
// Return the created user (excluding sensitive data)
return {
user: {
id: newUser.id,
name: newUser.name,
email: newUser.email,
createdAt: newUser.createdAt,
},
message: 'User created successfully',
};
},
});
Action Properties Reference
Here's a comprehensive breakdown of all available properties for Actions:
Property | Type | Required | Query | Mutation | Description |
---|---|---|---|---|---|
path | string | ✅ | ✅ | ✅ | URL path relative to controller. Supports parameters like /:id |
method | HTTPMethod | ❌ | ❌ | ✅ | HTTP method. Defaults to GET for queries, required for mutations |
handler | Function | ✅ | ✅ | ✅ | Async function containing your business logic |
query | StandardSchemaV1 | ❌ | ✅ | ✅ | Schema for validating URL query parameters |
body | StandardSchemaV1 | ❌ | ❌ | ✅ | Schema for validating request body data |
use | IgniterProcedure[] | ❌ | ✅ | ✅ | Array of procedure middleware to run before handler |
name | string | ❌ | ✅ | ✅ | Optional name for the action |
description | string | ❌ | ✅ | ✅ | Documentation description for API docs |
HTTP Methods Support
type HTTPMethod =
| 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
| 'HEAD' | 'OPTIONS' | 'TRACE';
type QueryMethod = 'GET'; // Queries are typically GET requests
type MutationMethod = Exclude<HTTPMethod, 'GET'>; // Mutations use other methods
3. The Context Object (ctx)
Every action handler receives a ctx
(context) object that provides access to all request data and utilities:
Context Anatomy
The context object (ctx
) passed to every action handler contains the following properties:
Property | Type | Description |
---|---|---|
request | Object | Contains all request-related data |
request.method | HTTPMethod | HTTP method used for the request |
request.path | string | Action path that was matched |
request.params | Object | URL parameters inferred from path (e.g., /:id → { id: string } ) |
request.headers | IgniterHeaders | Request headers with helper methods |
request.cookies | IgniterCookies | Request cookies with helper methods |
request.body | T | undefined | Validated request body (typed from schema) |
request.query | T | undefined | Validated query parameters (typed from schema) |
context | Object | Enhanced application context with global services |
response | IgniterResponseProcessor | Response processor for building HTTP responses |
realtime | IgniterRealtimeService | Service for real-time communication |
plugins | Object | Type-safe access to registered plugins |
Context Usage Examples
// Complete example with all context features
const getUserById = igniter.query({
path: '/users/:id',
query: z.object({
include: z.array(z.string()).optional(),
}),
handler: async (ctx) => {
// URL Parameters (typed from path)
const userId = ctx.request.params.id;
// Query Parameters (validated)
const includes = ctx.request.query?.include;
// Headers
const authToken = ctx.request.headers.get('authorization');
// Cookies
const sessionId = ctx.request.cookies.get('session-id');
// Set response cookies
ctx.response.setCookie('last-visited', new Date().toISOString(), {
httpOnly: true,
secure: true,
maxAge: 86400
});
// Access application context
const database = ctx.context.database;
// Business logic
const user = await database.user.findById(userId);
// Return response using response processor
return ctx.response.success({ user });
},
});
The ctx.query
and ctx.body
objects are fully typed based on your Zod schemas, providing excellent IntelliSense and compile-time safety.
4. Deep Dive: Creating a Query Action
Let's create a query
action to fetch a list of users, with support for pagination through query parameters.
// In src/features/user/controllers/user.controller.ts
import { igniter } from '@/igniter';
import { z } from 'zod';
export const userController = igniter.controller({
path: '/users',
actions: {
/**
* An action to list users.
* Final Path: GET /users/
*/
list: igniter.query({
path: '/',
// 1. Define and validate query parameters using Zod.
// These are optional and have default values.
query: z.object({
page: z.coerce.number().int().positive().optional().default(1),
limit: z.coerce.number().int().positive().optional().default(10),
}),
// 2. The main handler function.
handler: async ({ request, context, response }) => {
// `request.query` is fully typed by TypeScript as { page: number; limit: number; }
// based on the Zod schema above. No manual parsing or validation needed.
const { page, limit } = request.query;
const skip = (page - 1) * limit;
// Use the database client from the global context.
const users = await context.database.user.findMany({
take: limit,
skip: skip,
});
const totalUsers = await context.database.user.count();
// Use the response processor to return a structured, successful response.
return response.success({
users,
pagination: {
page,
limit,
total: totalUsers,
},
});
},
}),
},
});
In this example, Igniter.js automatically validates that page
and limit
are positive integers. If validation fails, it will return a 400 Bad Request
response with a descriptive error message before your handler code is ever executed.
4. Deep Dive: Creating a Mutation Action
Now, let's create a mutation
to add a new user to the database. This action will require authentication, which we'll enforce with a procedure.
// In src/features/user/controllers/user.controller.ts
import { igniter } from '@/igniter';
import { z } from 'zod';
import { auth } from '@/features/auth/procedures/auth.procedure'; // Assuming an auth procedure exists
export const userController = igniter.controller({
path: '/users',
actions: {
// ... (list action from above)
/**
* An action to create a new user.
* Final Path: POST /users/
*/
create: igniter.mutation({
path: '/',
method: 'POST',
// 1. Apply the 'auth' procedure to protect this route.
// This will run before the handler and can extend the context.
use: [auth],
// 2. Define and validate the request body using a Zod schema.
body: z.object({
name: z.string().min(2),
email: z.string().email(),
}),
// 3. The main handler function.
handler: async ({ request, context, response }) => {
// `request.body` is fully typed as { name: string; email: string; }
const { name, email } = request.body;
// `context.user` is available and typed here because the `auth`
// procedure added it to the context.
const createdBy = context.user;
context.logger.info(`User creation initiated by ${createdBy.email}`);
const newUser = await context.database.user.create({
data: { name, email },
});
// Use the `created` helper for a 201 Created status code.
return response.created(newUser);
},
}),
},
});
This mutation demonstrates the composability of Igniter.js. The validation, authentication, and business logic are all declared in a clean, readable, and type-safe way.
The Power of the ctx
Object
The ctx
object passed to every handler
is your unified gateway to everything you need for a request. It's an instance of IgniterActionContext
and contains:
ctx.request
: Fully-typed request data, includingparams
,query
,body
,headers
, andcookies
.ctx.context
: The dynamic application context, containing your global services (likedatabase
) and any data added by procedures (likeuser
).ctx.response
: The response processor for building type-safe HTTP responses (.success()
,.created()
,.unauthorized()
, etc.).ctx.plugins
: A type-safe entry point for interacting with any registered plugins.
By centralizing these concerns, Igniter.js allows you to focus purely on the business logic inside your handler.
Next Steps
Now you know how to build the core logic of your API. The next step is to understand the powerful middleware system that makes your code reusable and clean:
- Procedures (Middleware) - Learn about middleware
- Routing - Understand URL routing
- Validation - Type-safe input validation