Published on 2025-03-08
Creating a Controller
Published on
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:
// 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.
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.
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:
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.
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:
// 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.
// 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:
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
-
Keep Controllers Focused: Controllers should focus on handling HTTP requests and responses. Business logic should be delegated to services.
-
Use Procedures for Cross-Cutting Concerns: Use procedures for authentication, validation, logging, and other cross-cutting concerns.
-
Consistent Response Formats: Use the built-in response utilities to ensure consistent response formats across your API.
-
Leverage Type Safety: Use TypeScript's type system to ensure your API is type-safe.
-
Organize by Feature: Group controllers by feature or domain rather than by technical function.
-
Validate Input: Always validate input data before processing it.
-
Handle Errors Gracefully: Implement proper error handling to provide meaningful error messages to clients.
-
Use Dependency Injection: Inject services and other dependencies into your controllers to make them easier to test.
-
Write Tests: Write unit and integration tests for your controllers to ensure they work as expected.
Quick Tip
Always implement proper error handling and reconnection logic in your SSE clients to ensure a robust user experience.