Controllers
Controllers are the heart of your Balda.js 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-js';
import { Request, Response } from 'balda-js';
@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.js provides decorators for all HTTP methods:
GET Requests
@get('/')
async getAllUsers(req: Request, res: Response) {
res.json({ users: [] });
}
@get('/:id')
async getUserById(req: Request, res: Response) {
const id = req.params.id;
res.json({ user: { id } });
}
POST Requests
@post('/')
async createUser(req: Request, res: Response) {
const user = req.body;
res.created(user);
}
PUT Requests
@put('/:id')
async updateUser(req: Request, res: Response) {
const id = req.params.id;
const updates = req.body;
res.json({ id, ...updates });
}
PATCH Requests
@patch('/:id')
async partialUpdateUser(req: Request, res: Response) {
const id = req.params.id;
const updates = req.body;
res.json({ id, ...updates });
}
DELETE Requests
@del('/:id')
async deleteUser(req: Request, res: Response) {
const id = req.params.id;
res.noContent();
}
Route Options
Each HTTP method decorator can accept route options:
@get('/', {
middleware: [authMiddleware],
swagger: {
summary: 'Get all users',
description: 'Retrieve a list of all users'
}
})
async getAllUsers(req: Request, res: Response) {
res.json({ users: [] });
}
Controller-Level Middleware
You can apply middleware to all routes in a controller:
@controller('/users')
@middleware(authMiddleware)
export class UsersController {
@get('/')
async getAllUsers(req: Request, res: Response) {
// This route will use authMiddleware
res.json({ users: [] });
}
@get('/public')
@middleware(publicMiddleware) // Override controller middleware
async getPublicUsers(req: Request, res: Response) {
// This route will use publicMiddleware instead
res.json({ users: [] });
}
}
Complete Controller Example
import {
controller,
get,
post,
put,
del,
middleware,
validate,
serialize
} from 'balda-js';
import { Request, Response } from 'balda-js';
import z from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string()
});
const CreateUserSchema = z.object({
name: z.string(),
email: z.string().email()
});
@controller('/users')
@middleware(loggerMiddleware)
export class UsersController {
private users = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
@get('/')
@serialize(z.array(UserSchema))
async getAllUsers(req: Request, res: Response) {
res.json(this.users);
}
@get('/:id')
@serialize(UserSchema)
async getUserById(req: Request, res: Response) {
const id = parseInt(req.params.id);
const user = this.users.find(u => u.id === id);
if (!user) {
return res.notFound({ error: 'User not found' });
}
res.json(user);
}
@post('/')
@validate.body(CreateUserSchema)
@serialize(UserSchema)
async createUser(req: Request, res: Response, body: z.infer<typeof CreateUserSchema>) {
const newUser = {
id: this.users.length + 1,
...body
};
this.users.push(newUser);
res.created(newUser);
}
@put('/:id')
@middleware(authMiddleware)
@validate.body(CreateUserSchema)
@serialize(UserSchema)
async updateUser(req: Request, res: Response, body: z.infer<typeof CreateUserSchema>) {
const id = parseInt(req.params.id);
const userIndex = this.users.findIndex(u => u.id === id);
if (userIndex === -1) {
return res.notFound({ error: 'User not found' });
}
this.users[userIndex] = { ...this.users[userIndex], ...body };
res.json(this.users[userIndex]);
}
@del('/:id')
@middleware(authMiddleware)
async deleteUser(req: Request, res: Response) {
const id = parseInt(req.params.id);
const userIndex = this.users.findIndex(u => u.id === id);
if (userIndex === -1) {
return res.notFound({ error: 'User not found' });
}
this.users.splice(userIndex, 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
1. Single Responsibility
Each controller should handle a single resource or feature:
// Good: Focused on users
@controller('/users')
export class UsersController {}
// Good: Focused on authentication
@controller('/auth')
export class AuthController {}
// Avoid: Mixed responsibilities
@controller('/api')
export class ApiController {} // Too broad
2. Consistent Naming
Use consistent naming conventions:
// Good: Clear and descriptive
@controller('/users')
export class UsersController {}
@controller('/blog-posts')
export class BlogPostsController {}
// Avoid: Unclear names
@controller('/u')
export class UController {}
3. Error Handling
Handle errors consistently within controllers:
@controller('/users')
export class UsersController {
@get('/:id')
async getUserById(req: Request, res: Response) {
try {
const id = parseInt(req.params.id);
const user = await userService.findById(id);
if (!user) {
return res.notFound({ error: 'User not found' });
}
res.json(user);
} catch (error) {
console.error('Error fetching user:', error);
res.internalServerError({ error: 'Failed to fetch user' });
}
}
}
4. Validation and Serialization
Use validation and serialization decorators for type safety:
@controller('/users')
export class UsersController {
@post('/')
@validate.body(CreateUserSchema)
@serialize(UserSchema)
async createUser(req: Request, res: Response, body: z.infer<typeof CreateUserSchema>) {
// body is now typed as CreateUser
const user = await userService.create(body);
res.created(user);
}
}
5. Middleware Organization
Group related middleware and apply them at the appropriate level:
// Apply to all routes in controller
@controller('/users')
@middleware(authMiddleware)
export class UsersController {
// Apply to specific routes
@get('/admin')
@middleware(adminMiddleware)
async getAdminUsers(req: Request, res: Response) {
// Uses both authMiddleware and adminMiddleware
}
}
Controllers provide a clean, organized way to structure your Balda.js application's route handlers.