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.

Core Concepts

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:

PropertyTypeRequiredDescription
namestringOptional controller name for documentation and API introspection
pathstringBase URL path prefix for all actions in this controller
descriptionstringOptional description for documentation generation and OpenAPI spec
actionsRecord<string, Action>Collection of actions where keys are action names and values are action definitions
Type Safety

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

Code
// 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

PropertyTypeRequiredDescription
namestringOptional controller name for documentation and API introspection
pathstringBase URL path that prefixes all action paths
descriptionstringOptional description for documentation, OpenAPI spec, and MCP Server integration
actionsRecord<string, Action>Object containing all actions, where keys are action names
Path Composition

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:

PropertyTypeRequiredQueryMutationDescription
namestringOptional action name for documentation and MCP Server tool conversion
type'query' | 'mutation'Action type (automatically inferred from creation method)
pathstringURL path relative to controller, supports parameters like /:id
methodHTTPMethodHTTP method (defaults to GET for queries, required for mutations)
handlerFunctionAsync function containing your business logic
queryStandardSchemaV1Schema for validating URL query parameters
bodyStandardSchemaV1Schema for validating request body data
useIgniterProcedure[]Array of procedure middleware to run before handler
descriptionstringDocumentation description for OpenAPI docs and MCP Server
tagsstring[]Tags for categorization and documentation
$InferTActionInferInternal type inference helper (automatically managed)

Complete Action Examples

Query Action with Full Configuration

Code
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

Code
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

Complete API Reference

Here's a comprehensive breakdown of all available properties for Actions:

PropertyTypeRequiredQueryMutationDescription
pathstringURL path relative to controller. Supports parameters like /:id
methodHTTPMethodHTTP method. Defaults to GET for queries, required for mutations
handlerFunctionAsync function containing your business logic
queryStandardSchemaV1Schema for validating URL query parameters
bodyStandardSchemaV1Schema for validating request body data
useIgniterProcedure[]Array of procedure middleware to run before handler
namestringOptional name for the action
descriptionstringDocumentation description for API docs

HTTP Methods Support

Code
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:

PropertyTypeDescription
requestObjectContains all request-related data
request.methodHTTPMethodHTTP method used for the request
request.pathstringAction path that was matched
request.paramsObjectURL parameters inferred from path (e.g., /:id{ id: string })
request.headersIgniterHeadersRequest headers with helper methods
request.cookiesIgniterCookiesRequest cookies with helper methods
request.bodyT | undefinedValidated request body (typed from schema)
request.queryT | undefinedValidated query parameters (typed from schema)
contextObjectEnhanced application context with global services
responseIgniterResponseProcessorResponse processor for building HTTP responses
realtimeIgniterRealtimeServiceService for real-time communication
pluginsObjectType-safe access to registered plugins

Context Usage Examples

Code
// 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 });
  },
});
Type Safety

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.

Code
// 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.

Code
// 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, including params, query, body, headers, and cookies.
  • ctx.context: The dynamic application context, containing your global services (like database) and any data added by procedures (like user).
  • 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: