Skip to main content

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 controller
  • swaggerOptions (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.