Controllers
Controllers are the heart of your Balda application. They organize your route handlers into logical groups and provide a clean, decorator-based API for defining HTTP endpoints.
Basic Controller
import { controller, get, post } from 'balda';
import { Request, Response } from 'balda';
@controller('/users')
export class UsersController {
@get('/')
async getAllUsers(req: Request, res: Response) {
res.json({ users: [] });
}
@post('/')
async createUser(req: Request, res: Response) {
res.created(req.body);
}
}
Controller Decorator
The @controller() decorator marks a class as a controller and defines the base path for all routes within that controller.
@controller(path?: string, swaggerOptions?: SwaggerRouteOptions)
Parameters
path(optional): Base path for all routes in the controllerswaggerOptions(optional): Swagger documentation options for the controller
Examples
// Simple controller
@controller('/users')
export class UsersController {}
// Controller with Swagger options
@controller('/users', {
tags: ['Users'],
summary: 'User management endpoints'
})
export class UsersController {}
HTTP Method Decorators
Balda provides decorators for all HTTP methods:
@controller('/users')
export class UsersController {
@get('/')
getAll(req, res) {
res.json({ users: [] });
}
@get('/:id')
getById(req, res) {
res.json({ user: { id: req.params.id } });
}
@post('/')
create(req, res) {
res.created(req.body);
}
@put('/:id')
update(req, res) {
res.json({ id: req.params.id, ...req.body });
}
@patch('/:id')
partialUpdate(req, res) {
res.json({ id: req.params.id, ...req.body });
}
@del('/:id')
delete(req, res) {
res.noContent();
}
}
Route Options
HTTP method decorators accept various options:
// Middleware
@get('/', { middleware: [authMiddleware] })
getAll(req, res) { res.json({ users: [] }); }
// Body type for file uploads or form data
@post('/upload', { bodyType: 'form-data' })
upload(req, res) {
const file = req.file('fieldName');
res.ok({ filename: file.originalName });
}
// URL-encoded form data
@post('/form', { bodyType: 'urlencoded' })
handleForm(req, res) { res.ok(req.body); }
// Swagger documentation
@get('/', {
swagger: {
summary: 'Get all users',
description: 'Retrieve a list of all users'
}
})
getAll(req, res) { res.json({ users: [] }); }
Available Options
middleware: Middleware array for this routebodyType:'json'|'urlencoded'|'form-data'(default:'json')swagger: OpenAPI documentation metadata
Validation and Serialization
Balda supports Zod, TypeBox, and OpenAPI/AJV schemas for validation and serialization:
Request Validation
import { z } from 'zod';
import { Type } from '@sinclair/typebox';
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
// Or with TypeBox
const CreateUserTypeBox = Type.Object({
name: Type.String({ minLength: 1 }),
email: Type.String({ format: 'email' }),
});
// Or with OpenAPI/AJV
const CreateUserOpenApi = {
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string', format: 'email' },
},
required: ['name', 'email'],
} as const;
@controller('/users')
export class UsersController {
@post('/')
@validate.body(CreateUserSchema) // Body validation
create(req, res, body) {
// body is validated and typed
res.created(body);
}
@get('/')
@validate.query(z.object({ page: z.string().optional() })) // Query validation
getAll(req, res, query) {
// query is validated
res.json({ users: [], page: query.page });
}
}
Response Serialization
Multiple @serialize decorators can be stacked for different status codes:
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string(),
});
const ErrorSchema = z.object({
error: z.string(),
});
@controller('/users')
export class UsersController {
@get('/:id')
@serialize(UserSchema) // Default 200 response
@serialize(ErrorSchema, { status: 404 }) // 404 response
getById(req, res) {
const user = findUser(req.params.id);
user ? res.json(user) : res.notFound({ error: 'Not found' });
}
@post('/')
@serialize(UserSchema, { status: 201 }) // Success response
@serialize(ErrorSchema, { status: 409 }) // Conflict response
@serialize(ErrorSchema, { status: 400, safe: false }) // Unsafe validation (no check)
create(req, res, body) {
res.created(body);
}
}
Serialize Options
status: HTTP status code for this schema (default: 200)safe: Whether to validate the response (default: true)
Controller-Level Middleware
Apply middleware to all routes in a controller:
@controller('/users')
@middleware(authMiddleware)
export class UsersController {
@get('/')
getAll(req, res) {
// Uses authMiddleware
res.json({ users: [] });
}
@get('/public')
@middleware(publicMiddleware) // Override
getPublic(req, res) {
// Uses publicMiddleware instead
res.json({ users: [] });
}
}
Complete Controller Example
import { controller, get, post, put, del, middleware, validate, serialize } from 'balda';
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string(),
email: z.string().email()
});
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string()
});
@controller('/users')
@middleware(loggerMiddleware)
export class UsersController {
private users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' }
];
@get('/')
@serialize(z.array(UserSchema))
getAll(req, res) {
res.json(this.users);
}
@get('/:id')
@serialize(UserSchema)
getById(req, res) {
const user = this.users.find(u => u.id === Number(req.params.id));
user ? res.json(user) : res.notFound({ error: 'User not found' });
}
@post('/')
@validate.body(CreateUserSchema)
@serialize(UserSchema)
create(req, res, body) {
const user = { id: this.users.length + 1, ...body };
this.users.push(user);
res.created(user);
}
@put('/:id')
@middleware(authMiddleware)
@validate.body(CreateUserSchema)
update(req, res, body) {
const id = Number(req.params.id);
const index = this.users.findIndex(u => u.id === id);
if (index === -1) {
return res.notFound({ error: 'User not found' });
}
this.users[index] = { ...this.users[index], ...body };
res.json(this.users[index]);
}
@del('/:id')
@middleware(authMiddleware)
delete(req, res) {
const id = Number(req.params.id);
const index = this.users.findIndex(u => u.id === id);
if (index === -1) {
return res.notFound({ error: 'User not found' });
}
this.users.splice(index, 1);
res.noContent();
}
}
Controller Organization
File Structure
src/
├── controllers/
│ ├── users.controller.ts
│ ├── posts.controller.ts
│ ├── auth.controller.ts
│ └── index.ts
Controller Registration
Controllers are automatically registered when the server starts. Make sure your controller files match the patterns specified in your server configuration:
const server = new Server({
controllerPatterns: ['./src/controllers/**/*.ts']
});
Controller Index
You can create an index file to export all controllers:
// src/controllers/index.ts
export * from './users.controller';
export * from './posts.controller';
export * from './auth.controller';
Best Practices
- Single Responsibility - One controller per resource or feature
- Consistent Naming - Use clear, descriptive names (e.g.,
UsersController,AuthController) - Use Validation - Always validate request bodies with
@validate.body() - Use Serialization - Define response schemas with
@serialize()for type safety - Middleware Placement - Apply auth/logging at controller level, specific checks at route level
- Error Handling - Return appropriate HTTP status codes (
notFound,badRequest, etc.)
// Good example combining best practices
@controller('/users')
@middleware(loggerMiddleware)
export class UsersController {
@post('/')
@validate.body(CreateUserSchema)
@serialize(UserSchema)
create(req, res, body) {
const user = userService.create(body);
res.created(user);
}
}