Skip to main content

Controllers

Controllers are classes that organize related routes together. They use the @Controller decorator to define a base path for all routes within the class.

Creating a Controller

Use the @Controller(basePath) decorator to create a controller:

import { Controller, Get, Post } from 'muzu';

@Controller('/api/users')
class UserController {
@Get()
getAllUsers() {
return { users: [] };
}

@Get(':id')
getUser(req: Request) {
return { id: req.params.id };
}

@Post()
createUser(req: Request) {
return { created: true };
}
}

In this example:

  • GET /api/usersgetAllUsers()
  • GET /api/users/123getUser()
  • POST /api/userscreateUser()

Controller Registration

Controllers are automatically registered when their decorators execute:

import { MuzuServer, Controller, Get } from 'muzu';

// Controllers register themselves when @Controller executes
@Controller('/api')
class ApiController {
@Get('/status')
getStatus() {
return { status: 'ok' };
}
}

// Create server - controllers are already registered
const app = new MuzuServer();

// Controllers are loaded when listen() is called
app.listen(3000);
Auto-Registration

You don't need to manually register controllers. The @Controller decorator automatically adds the controller to a global registry, and all controllers are loaded when app.listen() is called.

Multiple Controllers

You can create multiple controllers for different resources:

@Controller('/api/users')
class UserController {
@Get()
getUsers() {
return { users: [] };
}

@Post()
createUser(req: Request) {
return { created: true };
}
}

@Controller('/api/products')
class ProductController {
@Get()
getProducts() {
return { products: [] };
}

@Get(':id')
getProduct(req: Request) {
return { product: {} };
}
}

@Controller('/api/orders')
class OrderController {
@Get()
getOrders() {
return { orders: [] };
}

@Post()
createOrder(req: Request) {
return { order: {} };
}
}

Controller Organization

Organize controllers by resource or feature:

Resource-Based Organization

// user.controller.ts
@Controller('/api/users')
export class UserController {
@Get()
getAll() { /* ... */ }

@Get(':id')
getById() { /* ... */ }

@Post()
create() { /* ... */ }

@Put(':id')
update() { /* ... */ }

@Delete(':id')
delete() { /* ... */ }
}

// product.controller.ts
@Controller('/api/products')
export class ProductController {
@Get()
getAll() { /* ... */ }

@Get(':id')
getById() { /* ... */ }
}

Feature-Based Organization

// auth.controller.ts
@Controller('/auth')
export class AuthController {
@Post('login')
login() { /* ... */ }

@Post('register')
register() { /* ... */ }

@Post('logout')
logout() { /* ... */ }
}

// profile.controller.ts
@Controller('/profile')
export class ProfileController {
@Get()
getProfile() { /* ... */ }

@Put()
updateProfile() { /* ... */ }
}

Controller with Services

Controllers should handle HTTP logic, delegate business logic to services:

// user.service.ts
class UserService {
async findAll() {
return await database.users.findAll();
}

async findById(id: string) {
return await database.users.findById(id);
}

async create(data: any) {
return await database.users.create(data);
}
}

// user.controller.ts
const userService = new UserService();

@Controller('/api/users')
class UserController {
@Get()
async getUsers() {
const users = await userService.findAll();
return { users };
}

@Get(':id')
async getUser(req: Request) {
const user = await userService.findById(req.params.id);

if (!user) {
throw new NotFoundException('User not found');
}

return user;
}

@Post()
async createUser(req: Request) {
const user = await userService.create(req.body);
return user;
}
}

Controller Base Path

The controller base path is combined with route paths:

@Controller('/api/v1/users')
class UserController {
@Get() // GET /api/v1/users
getAll() { /* ... */ }

@Get(':id') // GET /api/v1/users/:id
getById() { /* ... */ }

@Get(':id/posts') // GET /api/v1/users/:id/posts
getUserPosts() { /* ... */ }
}

Nested Routes

Create nested resource routes:

@Controller('/api/users')
class UserController {
@Get(':userId/posts')
getUserPosts(req: Request) {
const { userId } = req.params;
return { userId, posts: [] };
}

@Get(':userId/posts/:postId')
getUserPost(req: Request) {
const { userId, postId } = req.params;
return { userId, postId, post: {} };
}

@Get(':userId/posts/:postId/comments')
getPostComments(req: Request) {
const { userId, postId } = req.params;
return { userId, postId, comments: [] };
}
}

Controller Lifecycle

  1. Class Definition: Decorators execute when class is defined
  2. Controller Registration: @Controller adds class to global registry
  3. Server Start: app.listen() processes all registered controllers
  4. Route Compilation: Each route is compiled with optimized metadata
  5. Request Handling: Routes are matched using Radix tree
// 1. Class defined, decorators execute
@Controller('/api')
class ApiController {
@Get('/hello')
hello() {
return { message: 'Hello' };
}
}

// 2. Controller is now in global registry

const app = new MuzuServer();

// 3. listen() processes all controllers
app.listen(3000);

// 4. Routes are now compiled and ready
// 5. Server handles incoming requests

Testing Controllers

Clear registry between tests:

import { clearRegistry } from 'muzu';

describe('UserController', () => {
beforeEach(() => {
clearRegistry(); // Clear controller registry
});

it('should get users', () => {
@Controller('/users')
class UserController {
@Get()
getUsers() {
return { users: [] };
}
}

const app = new MuzuServer();
app.listen(3000);

// Test the controller...
});
});

Best Practices

Keep Controllers Thin

Controllers should only handle HTTP concerns:

// ✅ Good - Controller is thin
@Controller('/users')
class UserController {
@Get()
async getUsers() {
const users = await userService.findAll();
return { users };
}
}

// ❌ Bad - Controller has business logic
@Controller('/users')
class UserController {
@Get()
async getUsers() {
const users = await database.query('SELECT * FROM users');
const filtered = users.filter(u => u.active);
const sorted = filtered.sort((a, b) => a.name.localeCompare(b.name));
return { users: sorted };
}
}

Use Meaningful Names

// ✅ Good - Clear method names
@Controller('/products')
class ProductController {
@Get()
getAllProducts() { /* ... */ }

@Get(':id')
getProductById() { /* ... */ }

@Post()
createProduct() { /* ... */ }
}

// ❌ Bad - Unclear method names
@Controller('/products')
class ProductController {
@Get()
handler1() { /* ... */ }

@Get(':id')
handler2() { /* ... */ }
}
// ✅ Good - Related routes in same controller
@Controller('/users')
class UserController {
@Get()
getUsers() { /* ... */ }

@Get(':id')
getUser() { /* ... */ }

@Get(':id/posts')
getUserPosts() { /* ... */ }
}

// ❌ Bad - Related routes scattered
@Controller('/users')
class UserController {
@Get()
getUsers() { /* ... */ }
}

@Controller('/posts')
class PostController {
@Get('user/:id')
getUserPosts() { /* ... */ }
}

Next Steps