Controllers trong NestJS
Controller là lớp chịu trách nhiệm xử lý incoming requests và trả về responses cho client. Nó định tuyến các requests đến các service phù hợp để xử lý logic, sau đó trả về kết quả cho client.
Khái Niệm Controller
Controller là một class được trang trí bằng @Controller() decorator. Nó chứa các methods được trang trí bằng các HTTP method decorators (@Get(), @Post(), v.v.) để xử lý các HTTP requests.
import { Controller, Get } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.getAll();
}
}
Routing Cơ Bản
Khai Báo Route Path
@Controller('users')
export class UsersController {
// GET /users
@Get()
findAll() {
return ['Alice', 'Bob'];
}
// GET /users/profile
@Get('profile')
getProfile() {
return { name: 'John', email: 'john@example.com' };
}
// GET /users/:id
@Get(':id')
findOne(@Param('id') id: string) {
return { id, name: 'John' };
}
// POST /users
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
// PUT /users/:id
@Put(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(id, updateUserDto);
}
// DELETE /users/:id
@Delete(':id')
remove(@Param('id') id: string) {
return this.usersService.delete(id);
}
// PATCH /users/:id
@Patch(':id')
patch(@Param('id') id: string, @Body() patchUserDto: Partial<UpdateUserDto>) {
return this.usersService.patch(id, patchUserDto);
}
}
Nested Routes
@Controller('posts/:postId/comments')
export class CommentsController {
// GET /posts/:postId/comments
@Get()
findAll(@Param('postId') postId: string) {
return `Comments for post ${postId}`;
}
// GET /posts/:postId/comments/:id
@Get(':id')
findOne(@Param('postId') postId: string, @Param('id') id: string) {
return `Comment ${id} from post ${postId}`;
}
}
Wildcard Routes
@Controller('files')
export class FilesController {
// Khớp với /files/*, /files/*/*, v.v.
@Get('*')
serveFile(@Param(0) path: string) {
return `Serving file: ${path}`;
}
}
Extracting Request Data
@Param() - URL Parameters
@Controller('users')
export class UsersController {
// GET /users/123
@Get(':id')
findOne(@Param('id') id: string) {
return { id };
}
// GET /users/john/posts/5
@Get(':username/posts/:postId')
getUserPost(
@Param('username') username: string,
@Param('postId') postId: string,
) {
return { username, postId };
}
// Lấy tất cả parameters
@Get(':id/details/:detail')
getDetails(@Param() params: any) {
return params; // { id: '123', detail: 'profile' }
}
}
@Query() - Query String Parameters
@Controller('users')
export class UsersController {
// GET /users?page=1&limit=10
@Get()
findAll(
@Query('page') page: string = '1',
@Query('limit') limit: string = '10',
) {
return { page: parseInt(page), limit: parseInt(limit) };
}
// GET /users?search=john&status=active&sort=name
@Get('search')
search(@Query() query: any) {
return query; // { search: 'john', status: 'active', sort: 'name' }
}
}
@Body() - Request Body
// dto/create-user.dto.ts
export class CreateUserDto {
name: string;
email: string;
age: number;
}
@Controller('users')
export class UsersController {
// POST /users
// Body: { "name": "John", "email": "john@example.com", "age": 30 }
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
// POST /users/bulk
@Post('bulk')
createBulk(@Body() createUserDtos: CreateUserDto[]) {
return this.usersService.createMany(createUserDtos);
}
}
@Headers() - Request Headers
@Controller('auth')
export class AuthController {
// GET /auth/profile
// Headers: Authorization: Bearer token123
@Get('profile')
getProfile(@Headers('authorization') auth: string) {
return { token: auth };
}
// Lấy tất cả headers
@Get('headers')
getHeaders(@Headers() headers: any) {
return headers;
}
}
@Req() và @Res() - Express Objects
import { Request, Response } from 'express';
@Controller('files')
export class FilesController {
// Sử dụng Express Request/Response trực tiếp
@Get(':filename')
downloadFile(
@Req() req: Request,
@Res() res: Response,
@Param('filename') filename: string,
) {
res.download(`./files/${filename}`);
}
// Custom response handling
@Post('upload')
uploadFile(@Req() req: Request, @Res() res: Response) {
// Xử lý multipart/form-data
const file = req.files[0];
res.status(201).json({ message: 'File uploaded', file });
}
}
@Ip() - Client IP Address
@Controller('api')
export class ApiController {
@Get('info')
getInfo(@Ip() ip: string) {
return { clientIp: ip };
}
}
@HostParam() - Subdomain Routing
@Controller()
export class HostBasedController {
// GET http://api.example.com/users
@Get('users')
@HostParam('subdomain')
getSubdomainUsers(@HostParam() host: string) {
return `Users from ${host}`;
}
}
// Trong app.module.ts
@Module({
controllers: [HostBasedController],
})
export class AppModule {}
Response Handling
Automatic Response Serialization
@Controller('users')
export class UsersController {
// Trả về object - NestJS tự động serialize thành JSON
@Get()
findAll() {
return { message: 'Success', data: [] };
}
// Trả về array
@Get('list')
list() {
return ['Alice', 'Bob', 'Charlie'];
}
// Trả về string
@Get('hello')
hello() {
return 'Hello World';
}
}
Custom Status Codes
@Controller('users')
export class UsersController {
// POST /users - trả về 201 Created
@Post()
@HttpCode(201)
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
// DELETE /users/:id - trả về 204 No Content
@Delete(':id')
@HttpCode(204)
remove(@Param('id') id: string) {
return this.usersService.delete(id);
}
// Custom status dynamically
@Get(':id')
findOne(@Param('id') id: string) {
const user = this.usersService.findOne(id);
if (!user) {
throw new NotFoundException('User not found');
}
return user;
}
}
Custom Headers
import { Header } from '@nestjs/common';
@Controller('files')
export class FilesController {
@Get('document')
@Header('Content-Type', 'application/pdf')
@Header('Content-Disposition', 'attachment; filename="document.pdf"')
downloadDocument() {
// Trả về PDF content
return Buffer.from('PDF content');
}
@Get('data')
@Header('X-Custom-Header', 'CustomValue')
getData() {
return { data: 'something' };
}
}
Streaming Responses
import { StreamableFile } from '@nestjs/common';
import { createReadStream } from 'fs';
import { join } from 'path';
@Controller('files')
export class FilesController {
@Get('stream/:filename')
getFile(@Param('filename') filename: string) {
const file = createReadStream(join(process.cwd(), `uploads/${filename}`));
return new StreamableFile(file);
}
@Get('image/:id')
getImage(@Param('id') id: string) {
const file = createReadStream(join(process.cwd(), `images/${id}.png`));
return new StreamableFile(file, {
type: 'image/png',
});
}
}
Sending Files
@Controller('download')
export class DownloadController {
@Get(':filename')
downloadFile(
@Param('filename') filename: string,
@Res() res: Response,
) {
const filePath = join(process.cwd(), `files/${filename}`);
return res.download(filePath);
}
}
Data Transfer Objects (DTOs)
DTO là class định nghĩa structure của request/response data:
// dto/create-user.dto.ts
export class CreateUserDto {
name: string;
email: string;
age: number;
role?: string; // Optional
}
// dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
// controllers/users.controller.ts
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Put(':id')
update(
@Param('id') id: string,
@Body() updateUserDto: UpdateUserDto,
) {
return this.usersService.update(id, updateUserDto);
}
}
DTOs với Validation
import { IsString, IsEmail, IsNumber, IsOptional, Min, Max } from 'class-validator';
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@IsNumber()
@Min(18)
@Max(120)
age: number;
@IsOptional()
@IsString()
role?: string;
}
Ví dụ Thực Tế - REST API Hoàn Chỉnh
// users.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Param,
Body,
Query,
HttpCode,
HttpStatus,
NotFoundException,
BadRequestException,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('api/users')
export class UsersController {
constructor(private usersService: UsersService) {}
// GET /api/users?page=1&limit=10
@Get()
async findAll(
@Query('page') page: string = '1',
@Query('limit') limit: string = '10',
) {
const pageNum = parseInt(page);
const limitNum = parseInt(limit);
if (isNaN(pageNum) || isNaN(limitNum)) {
throw new BadRequestException('Page and limit must be numbers');
}
return this.usersService.findAll(pageNum, limitNum);
}
// GET /api/users/search?keyword=john
@Get('search')
async search(@Query('keyword') keyword: string) {
if (!keyword) {
throw new BadRequestException('Keyword is required');
}
return this.usersService.search(keyword);
}
// GET /api/users/:id
@Get(':id')
async findOne(@Param('id') id: string) {
const user = await this.usersService.findOne(id);
if (!user) {
throw new NotFoundException(`User with id ${id} not found`);
}
return user;
}
// POST /api/users
@Post()
@HttpCode(HttpStatus.CREATED)
async create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
// PUT /api/users/:id
@Put(':id')
async update(
@Param('id') id: string,
@Body() updateUserDto: UpdateUserDto,
) {
const user = await this.usersService.findOne(id);
if (!user) {
throw new NotFoundException(`User with id ${id} not found`);
}
return this.usersService.update(id, updateUserDto);
}
// DELETE /api/users/:id
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id') id: string) {
const user = await this.usersService.findOne(id);
if (!user) {
throw new NotFoundException(`User with id ${id} not found`);
}
return this.usersService.delete(id);
}
// GET /api/users/:id/posts
@Get(':id/posts')
async getUserPosts(@Param('id') id: string) {
const user = await this.usersService.findOne(id);
if (!user) {
throw new NotFoundException(`User with id ${id} not found`);
}
return this.usersService.getUserPosts(id);
}
}
Asynchronous Endpoints
Controllers có thể xử lý async operations:
@Controller('data')
export class DataController {
constructor(private databaseService: DatabaseService) {}
// Trả về Promise
@Get()
async getData() {
return await this.databaseService.find();
}
// Trả về Observable (RxJS)
@Get('stream')
getDataStream() {
return this.databaseService.findAsObservable();
}
}
Redirects
import { Redirect } from '@nestjs/common';
@Controller('redirects')
export class RedirectController {
// Redirect cứng
@Get('to-google')
@Redirect('https://google.com', 301)
redirectToGoogle() {}
// Redirect động
@Get(':id/redirect')
@Redirect()
redirect(@Param('id') id: string) {
return {
url: `/users/${id}`,
statusCode: 302,
};
}
}
Best Practices
1. Tổ Chức Controllers theo Features
src/
├── users/
│ ├── users.controller.ts
│ ├── users.service.ts
│ └── users.module.ts
├── products/
│ ├── products.controller.ts
│ ├── products.service.ts
│ └── products.module.ts
└── app.module.ts
2. Sử Dụng DTOs cho Type Safety
// ❌ Sai
@Post()
create(@Body() body: any) {
return this.service.create(body);
}
// ✅ Đúng
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.service.create(createUserDto);
}
3. Consistent Response Format
// Tạo base response class
export class ResponseDto<T> {
success: boolean;
message: string;
data?: T;
error?: string;
}
// Sử dụng consistent response
@Get()
async findAll(): Promise<ResponseDto<User[]>> {
try {
const users = await this.usersService.findAll();
return {
success: true,
message: 'Users retrieved successfully',
data: users,
};
} catch (error) {
return {
success: false,
message: 'Failed to retrieve users',
error: error.message,
};
}
}
4. Error Handling
// ❌ Sai
@Get(':id')
findOne(@Param('id') id: string) {
const user = this.usersService.findOne(id);
return user; // Có thể undefined
}
// ✅ Đúng
@Get(':id')
async findOne(@Param('id') id: string) {
const user = await this.usersService.findOne(id);
if (!user) {
throw new NotFoundException('User not found');
}
return user;
}
5. Proper HTTP Status Codes
@Post()
@HttpCode(HttpStatus.CREATED) // 201
create(@Body() createDto: CreateUserDto) {
return this.service.create(createDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) // 204
delete(@Param('id') id: string) {
return this.service.delete(id);
}
6. Validate Input Data
import { ValidationPipe } from '@nestjs/common';
// Trong main.ts
app.useGlobalPipes(new ValidationPipe());
// Hoặc cho từng route
@Post()
create(
@Body(new ValidationPipe()) createUserDto: CreateUserDto,
) {
return this.service.create(createUserDto);
}
Testing Controllers
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
describe('UsersController', () => {
let controller: UsersController;
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: {
findAll: jest.fn().mockResolvedValue([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]),
},
},
],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
});
describe('findAll', () => {
it('should return array of users', async () => {
const result = await controller.findAll();
expect(result).toEqual([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]);
});
});
});
Kết Luận
Controllers là giao diện giữa client và logic ứng dụng của bạn. Hiểu rõ về controllers giúp bạn:
- Xây dựng API RESTful chuyên nghiệp
- Xử lý requests và responses một cách rõ ràng
- Validate input data một cách hiệu quả
- Trả về appropriate HTTP status codes
- Xây dựng ứng dụng dễ test và bảo trì
Sử dụng controllers đúng cách là cơ sở để xây dựng các NestJS applications mạnh mẽ và scalable.