Controllers
Learn about the controller pattern in AzuraJS
Controllers 🎯
Controllers are the heart of AzuraJS applications. They organize your route handlers into logical, reusable classes using TypeScript decorators.
What is a Controller? 📚
A controller is a class decorated with @Controller that groups related route handlers together. Each method in the controller handles a specific route.
import { Controller, Get, Post } from "azurajs/decorators";
@Controller("/api/products")
export class ProductController {
@Get()
getAllProducts() {
return { products: [] };
}
@Post()
createProduct() {
return { message: "Product created" };
}
}Controller Decorator 🏷️
The @Controller decorator defines a base path for all routes in the controller.
Basic Usage
@Controller("/api/users")
export class UserController {
// Routes will be prefixed with /api/users
}Without Prefix
@Controller() // No prefix
export class RootController {
@Get("/health") // Route: /health
healthCheck() {
return { status: "ok" };
}
}Nested Paths
@Controller("/api/v1/admin")
export class AdminController {
@Get("/users") // Route: /api/v1/admin/users
getUsers() {
return { users: [] };
}
}Creating Controllers 🛠️
Step 1: Create the Controller Class
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Res
} from "azurajs/decorators";
import type { ResponseServer } from "azurajs";
@Controller("/api/users")
export class UserController {
@Get()
getAllUsers(@Res() res: ResponseServer) {
res.json({ users: [] });
}
@Get("/:id")
getUser(@Param("id") id: string, @Res() res: ResponseServer) {
res.json({ id, name: `User ${id}` });
}
@Post()
createUser(@Body() data: any, @Res() res: ResponseServer) {
res.status(201).json({ id: Date.now(), ...data });
}
@Put("/:id")
updateUser(
@Param("id") id: string,
@Body() data: any,
@Res() res: ResponseServer
) {
res.json({ id, ...data });
}
@Delete("/:id")
deleteUser(@Param("id") id: string, @Res() res: ResponseServer) {
res.json({ message: "User deleted" });
}
}Step 2: Register the Controller
import { AzuraClient, applyDecorators } from "azurajs";
import { UserController } from "./controllers/UserController";
const app = new AzuraClient();
// Register single controller
applyDecorators(app, [UserController]);
await app.listen();Multiple Controllers 📦
Register multiple controllers at once:
import { AzuraClient, applyDecorators } from "azurajs";
import { UserController } from "./controllers/UserController";
import { ProductController } from "./controllers/ProductController";
import { AuthController } from "./controllers/AuthController";
const app = new AzuraClient();
applyDecorators(app, [
UserController,
ProductController,
AuthController,
]);
await app.listen();Controller Organization 📁
Organize controllers by feature or resource:
src/
├── controllers/
│ ├── index.ts
│ ├── UserController.ts
│ ├── ProductController.ts
│ ├── OrderController.ts
│ └── AuthController.ts
├── services/
│ ├── UserService.ts
│ └── ProductService.ts
└── index.tsCreate a barrel export:
export { UserController } from "./UserController";
export { ProductController } from "./ProductController";
export { OrderController } from "./OrderController";
export { AuthController } from "./AuthController";Then import all at once:
import { AzuraClient, applyDecorators } from "azurajs";
import * as controllers from "./controllers";
const app = new AzuraClient();
applyDecorators(app, Object.values(controllers));
await app.listen();Controller Best Practices ✨
1. Single Responsibility
Each controller should handle one resource or feature:
// ✅ Good: Focused on users
@Controller("/api/users")
export class UserController {
@Get() getAll() {}
@Post() create() {}
}
// ❌ Bad: Mixed responsibilities
@Controller("/api")
export class ApiController {
@Get("/users") getUsers() {}
@Get("/products") getProducts() {}
@Get("/orders") getOrders() {}
}2. Use Services for Business Logic
Keep controllers thin by delegating to services:
// UserService.ts
export class UserService {
async getAllUsers() {
// Business logic here
return await database.users.findMany();
}
async createUser(data: any) {
// Validation and creation logic
return await database.users.create(data);
}
}
// UserController.ts
import { UserService } from "../services/UserService";
@Controller("/api/users")
export class UserController {
private userService = new UserService();
@Get()
async getAllUsers(@Res() res: ResponseServer) {
const users = await this.userService.getAllUsers();
res.json({ users });
}
@Post()
async createUser(@Body() data: any, @Res() res: ResponseServer) {
const user = await this.userService.createUser(data);
res.status(201).json({ user });
}
}3. Consistent Response Format
Use a consistent response structure:
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
@Controller("/api/users")
export class UserController {
@Get()
getAll(@Res() res: ResponseServer) {
res.json({
success: true,
data: users
});
}
@Get("/:id")
getOne(@Param("id") id: string, @Res() res: ResponseServer) {
const user = users.find(u => u.id === id);
if (!user) {
return res.status(404).json({
success: false,
error: "User not found"
});
}
res.json({
success: true,
data: user
});
}
}4. Error Handling
Use try-catch blocks and consistent error responses:
import { Controller, Post, Body, Res } from "azurajs/decorators";
import { HttpError } from "azurajs/http-error";
import type { ResponseServer } from "azurajs";
@Controller("/api/users")
export class UserController {
@Post()
async createUser(@Body() data: any, @Res() res: ResponseServer) {
try {
// Validate
if (!data.email) {
throw new HttpError(400, "Email is required");
}
// Create user
const user = await this.userService.createUser(data);
res.status(201).json({
success: true,
data: user
});
} catch (error) {
if (error instanceof HttpError) {
res.status(error.status).json({
success: false,
error: error.message
});
} else {
res.status(500).json({
success: false,
error: "Internal server error"
});
}
}
}
}5. Type Safety
Use TypeScript interfaces for request/response types:
interface CreateUserDto {
name: string;
email: string;
password: string;
}
interface User {
id: number;
name: string;
email: string;
createdAt: Date;
}
@Controller("/api/users")
export class UserController {
@Post()
async createUser(
@Body() data: CreateUserDto,
@Res() res: ResponseServer
) {
const user: User = await this.userService.createUser(data);
res.status(201).json({ user });
}
}Advanced Patterns 🚀
Constructor Injection
Use constructor to inject dependencies:
import { UserService } from "../services/UserService";
import { Logger } from "../utils/Logger";
@Controller("/api/users")
export class UserController {
constructor(
private userService: UserService,
private logger: Logger
) {}
@Get()
async getAll(@Res() res: ResponseServer) {
this.logger.info("Fetching all users");
const users = await this.userService.getAllUsers();
res.json({ users });
}
}Shared Methods
Create shared methods for common operations:
@Controller("/api/users")
export class UserController {
private userService = new UserService();
private sendSuccess(res: ResponseServer, data: any, status = 200) {
res.status(status).json({
success: true,
data
});
}
private sendError(res: ResponseServer, message: string, status = 400) {
res.status(status).json({
success: false,
error: message
});
}
@Get()
async getAll(@Res() res: ResponseServer) {
const users = await this.userService.getAllUsers();
this.sendSuccess(res, users);
}
@Post()
async create(@Body() data: any, @Res() res: ResponseServer) {
try {
const user = await this.userService.createUser(data);
this.sendSuccess(res, user, 201);
} catch (error: any) {
this.sendError(res, error.message, 400);
}
}
}