Published on 2025-03-08

Creating a Controller

Published on

2025-03-08

Controllers

Controllers are a fundamental part of the Igniter framework. They handle incoming HTTP requests, process data, and return appropriate responses. Controllers in Igniter are designed to be type-safe, modular, and easy to test.

Creating a Controller

To create a controller in Igniter, you use the igniter.controller() function. Here's a basic example:

typescript
// src/features/user/controllers/user.controller.ts
import { igniter } from '@/igniter'
import { userService } from '../services/user.service'

export const userController = igniter.controller({
  path: '/users',
  actions: {
    list: igniter.query({
      path: '/',
      handler: async (ctx) => {
        const users = await userService.findAll()
        return ctx.response.ok(users)
      }
    }),
    get: igniter.query({
      path: '/:id',
      handler: async (ctx) => {
        const { id } = ctx.params
        const user = await userService.findById(id)
        
        if (!user) {
          return ctx.response.notFound('User not found')
        }
        
        return ctx.response.ok(user)
      }
    }),
    create: igniter.mutation({
      path: '/',
      method: 'POST',
      handler: async (ctx) => {
        const data = ctx.body
        const user = await userService.create(data)
        return ctx.response.created(user)
      }
    }),
    update: igniter.mutation({
      path: '/:id',
      method: 'PUT',
      handler: async (ctx) => {
        const { id } = ctx.params
        const data = ctx.body
        const user = await userService.update(id, data)
        
        if (!user) {
          return ctx.response.notFound('User not found')
        }
        
        return ctx.response.ok(user)
      }
    }),
    delete: igniter.mutation({
      path: '/:id',
      method: 'DELETE',
      handler: async (ctx) => {
        const { id } = ctx.params
        await userService.delete(id)
        return ctx.response.noContent()
      }
    })
  }
})

The controller configuration includes:

  • path: The base path for all actions in the controller (e.g., /users)
  • actions: An object containing all the actions (endpoints) for the controller

Controller Actions

Each action in a controller represents an endpoint. Igniter provides two types of actions:

Queries

Queries are used for read operations (GET requests). They don't modify data and are safe to call multiple times.

typescript
list: igniter.query({
  path: '/',
  handler: async (ctx) => {
    // Handle GET request
  }
})

Mutations

Mutations are used for write operations (POST, PUT, DELETE requests). They modify data and should be idempotent when possible.

typescript
create: igniter.mutation({
  path: '/',
  method: 'POST',
  handler: async (ctx) => {
    // Handle POST request
  }
})

Request Context

Each controller action receives a context object (ctx) that provides access to the request data and utilities for generating responses:

typescript
handler: async (ctx) => {
  // Request data
  const params = ctx.params // URL parameters
  const query = ctx.query   // Query string parameters
  const body = ctx.body     // Request body
  const headers = ctx.headers // Request headers
  
  // Response utilities
  return ctx.response.ok(data) // 200 OK response
  // Other response methods: created, noContent, badRequest, unauthorized, forbidden, notFound, etc.
}

Using Procedures with Controllers

Procedures are middleware functions that can be applied to controller actions. They allow you to add cross-cutting concerns like authentication, validation, and logging.

typescript
import { igniter } from '@/igniter'
import { authProcedure } from '@/procedures/auth.procedure'
import { validateUserProcedure } from '@/procedures/validate-user.procedure'

export const userController = igniter.controller({
  path: '/users',
  actions: {
    create: igniter.mutation({
      path: '/',
      method: 'POST',
      use: [
        authProcedure(), // Authenticate the request
        validateUserProcedure() // Validate the user data
      ],
      handler: async (ctx) => {
        // Handle the request
      }
    })
  }
})

Handling Responses

Igniter provides a set of response utilities to make it easy to return standardized HTTP responses:

typescript
// Success responses
ctx.response.ok(data) // 200 OK
ctx.response.created(data) // 201 Created
ctx.response.accepted() // 202 Accepted
ctx.response.noContent() // 204 No Content

// Client error responses
ctx.response.badRequest(message) // 400 Bad Request
ctx.response.unauthorized(message) // 401 Unauthorized
ctx.response.forbidden(message) // 403 Forbidden
ctx.response.notFound(message) // 404 Not Found
ctx.response.methodNotAllowed(message) // 405 Method Not Allowed
ctx.response.conflict(message) // 409 Conflict

// Server error responses
ctx.response.internalServerError(message) // 500 Internal Server Error
ctx.response.notImplemented(message) // 501 Not Implemented
ctx.response.badGateway(message) // 502 Bad Gateway
ctx.response.serviceUnavailable(message) // 503 Service Unavailable

Type Safety

One of the key benefits of Igniter controllers is type safety. The framework provides full type inference for your controller actions, ensuring that your client code is always in sync with your server code.

typescript
// Define input and output types for your controller actions
import { z } from 'zod'

const createUserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  password: z.string().min(8)
})

type CreateUserInput = z.infer<typeof createUserSchema>
type User = { id: string; name: string; email: string }

export const userController = igniter.controller({
  path: '/users',
  actions: {
    create: igniter.mutation<CreateUserInput, User>({
      path: '/',
      method: 'POST',
      handler: async (ctx) => {
        // ctx.body is typed as CreateUserInput
        const user = await userService.create(ctx.body)
        // Return value is typed as User
        return ctx.response.created(user)
      }
    })
  }
})

Controller Factories

You can create controller factories to generate controllers with similar behavior:

typescript
function createCrudController<T>(options: {
  path: string,
  service: CrudService<T>
}) {
  return igniter.controller({
    path: options.path,
    actions: {
      list: igniter.query({
        path: '/',
        handler: async (ctx) => {
          const items = await options.service.findAll()
          return ctx.response.ok(items)
        }
      }),
      get: igniter.query({
        path: '/:id',
        handler: async (ctx) => {
          const { id } = ctx.params
          const item = await options.service.findById(id)
          
          if (!item) {
            return ctx.response.notFound('Item not found')
          }
          
          return ctx.response.ok(item)
        }
      }),
      // Add other CRUD actions...
    }
  })
}

// Usage
export const userController = createCrudController({
  path: '/users',
  service: userService
})

Best Practices

  1. Keep Controllers Focused: Controllers should focus on handling HTTP requests and responses. Business logic should be delegated to services.

  2. Use Procedures for Cross-Cutting Concerns: Use procedures for authentication, validation, logging, and other cross-cutting concerns.

  3. Consistent Response Formats: Use the built-in response utilities to ensure consistent response formats across your API.

  4. Leverage Type Safety: Use TypeScript's type system to ensure your API is type-safe.

  5. Organize by Feature: Group controllers by feature or domain rather than by technical function.

  6. Validate Input: Always validate input data before processing it.

  7. Handle Errors Gracefully: Implement proper error handling to provide meaningful error messages to clients.

  8. Use Dependency Injection: Inject services and other dependencies into your controllers to make them easier to test.

  9. Write Tests: Write unit and integration tests for your controllers to ensure they work as expected.

AD

Quick Tip

Always implement proper error handling and reconnection logic in your SSE clients to ensure a robust user experience.

You might also like