Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026
NestJS
NestJS là framework Node.js xây dựng trên TypeScript, lấy cảm hứng từ Angular. Nó cung cấp kiến trúc module rõ ràng với @Module(), @Controller(), @Injectable() và Dependency Injection tích hợp sẵn.
Lý do chọn NestJS thay Express thuần: kiến trúc có cấu trúc (Modules/Controllers/Services phân tách rõ ràng), TypeScript-first với decorators và metadata reflection, IoC container tích hợp dễ test, hỗ trợ Microservices/GraphQL/WebSockets/gRPC. NestJS vẫn chạy trên Express (hoặc Fastify) bên dưới, nhưng thêm lớp abstraction giúp code dễ maintain và scale hơn.
Module là đơn vị tổ chức cơ bản trong NestJS, nhóm các thành phần liên quan lại. Mỗi app có ít nhất một root module (AppModule).
@Module() nhận một object với 4 thuộc tính: imports (modules khác cần dùng), controllers (xử lý HTTP requests), providers (services, repositories, guards...), và exports (providers cho phép modules khác sử dụng). Chỉ những providers được exports mới có thể được inject ở module khác.
Các loại module: Feature Module nhóm theo tính năng (UsersModule, AuthModule), Shared Module export providers để tái sử dụng, Global Module dùng @Global() để providers available toàn app không cần import, Dynamic Module cấu hình runtime qua forRoot() / forRootAsync().
Controller chịu trách nhiệm nhận HTTP requests và trả về responses. Controller map routes đến handler methods thông qua decorators.
@Controller('users') đặt base route /users. Các HTTP method decorators: @Get(), @Post(), @Patch(), @Put(), @Delete(). Có thể thêm path vào decorator như @Get(':id') để tạo route động.
Parameter decorators để extract data từ request: @Param('id') lấy route param, @Query() lấy query string, @Body() lấy request body, @Headers() lấy headers, @Req() / @Res() để access raw request/response (dùng @Res() sẽ mất một số tính năng NestJS như interceptors).
Provider là bất kỳ class nào được annotate với @Injectable() — services, repositories, factories, helpers. NestJS quản lý vòng đời và inject chúng tự động thông qua constructor injection.
Cách hoạt động: khai báo provider trong providers array của module, NestJS IoC container tạo instance và inject vào các class phụ thuộc qua constructor. Reflector đọc TypeScript metadata để biết type cần inject.
Scope của providers: DEFAULT (Singleton) — một instance cho toàn app, REQUEST — instance mới cho mỗi request, TRANSIENT — instance mới mỗi lần inject. Custom providers cho phép linh hoạt hơn: useValue để inject giá trị cụ thể, useFactory để tạo provider với logic phức tạp, useClass để swap implementation.
@nestjs/config giúp quản lý environment variables an toàn với type-safety. ConfigModule.forRoot() với isGlobal: true cho phép inject ConfigService ở bất kỳ module nào mà không cần import lại.
Validation schema với Joi: validationSchema: Joi.object({ PORT: Joi.number().default(3000), JWT_SECRET: Joi.string().min(32).required() }) — app sẽ fail ngay khi start nếu env vars thiếu hoặc sai format.
Namespaced config với registerAs('database', () => ({ host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT) })) cho phép nhóm config và inject type-safe qua @Inject(databaseConfig.KEY) private dbConfig: ConfigType<typeof databaseConfig>. ConfigService.get<string>('KEY') dùng khi không cần namespace, ConfigService.get<string>('database.host') với dot notation cho nested config.
NestJS tích hợp Swagger qua @nestjs/swagger. Setup trong main.ts: DocumentBuilder cấu hình title/description/version/auth, SwaggerModule.createDocument() tạo document, SwaggerModule.setup('api/docs', app, document) serve UI.
Annotations trên DTO: @ApiProperty({ example, description }) cho required fields, @ApiPropertyOptional() cho optional fields. Hỗ trợ enum, type, minLength, maxLength, default.
Annotations trên Controller: @ApiTags('GroupName') nhóm endpoints trong Swagger UI, @ApiBearerAuth() chỉ định cần JWT, @ApiOperation({ summary }) mô tả endpoint, @ApiResponse({ status, type, description }) document các response codes. Param annotations: @ApiParam() cho route params, @ApiQuery() cho query params. PickType, OmitType, PartialType từ @nestjs/swagger để tạo derived DTOs tái sử dụng schema.
NestJS xử lý request qua pipeline theo thứ tự cố định: Middleware chạy đầu tiên (logging, CORS, session — access raw req/res), tiếp theo Guards kiểm tra authorization (trả về true/false), rồi Interceptors pre-processing (transform request trước khi vào handler), sau đó Pipes validate và transform input data, tiếp theo Controller Handler thực thi business logic chính, rồi Interceptors post-processing (transform response), cuối cùng Exception Filters bắt và format errors thành response chuẩn.
Middleware và Guards đều chạy trước handler nhưng Guards có access vào ExecutionContext nên biết handler nào sẽ được gọi. Exception Filters chỉ hoạt động khi có exception, nếu không có lỗi thì bỏ qua. Mỗi layer có thể áp dụng theo thứ tự: global → controller → route.
Guards quyết định request có được phép đi tiếp không (authorization). Khác với Middleware, Guards implement interface CanActivate và có access vào ExecutionContext — biết được handler nào sẽ được gọi, rất hữu ích cho role-based access control.
JWT Auth Guard hoạt động: extract Bearer token từ header Authorization, verify token bằng JwtService.verify(), nếu hợp lệ attach payload vào request.user và trả về true, ngược lại throw UnauthorizedException. Guards có thể áp dụng với @UseGuards() ở mức route, controller, hoặc global qua APP_GUARD provider.
Public decorator pattern: dùng SetMetadata('isPublic', true) với @Public() decorator, trong guard đọc metadata qua Reflector để bỏ qua authentication cho các route công khai.
Pipes có hai use-case chính: validation (throw exception nếu data không hợp lệ) và transformation (chuyển đổi input sang dạng mong muốn). Pipes implement interface PipeTransform với method transform(value, metadata).
ValidationPipe của NestJS kết hợp với class-validator để validate DTO tự động. Cấu hình quan trọng: whitelist: true loại bỏ các properties không khai báo trong DTO, forbidNonWhitelisted: true throw error thay vì strip, transform: true tự động chuyển đổi type (string sang number), enableImplicitConversion: true convert dựa trên TypeScript type.
DTO dùng decorators từ class-validator như @IsString(), @IsEmail(), @MinLength(), @IsOptional(), @IsEnum(). class-transformer cung cấp @Transform() để biến đổi giá trị trước khi validate. Pipe global đăng ký qua app.useGlobalPipes() hoặc APP_PIPE provider.
Interceptors wrap việc thực thi handler, cho phép chạy code trước và sau handler. Chúng implement NestInterceptor với method intercept(context, next) trả về Observable. Gọi next.handle() để tiếp tục pipeline, dùng RxJS operators để transform.
Use-cases phổ biến: Response transform — dùng map() để wrap tất cả response trong object chuẩn { success: true, data, timestamp }. Logging — ghi thời gian xử lý với tap(). Caching — kiểm tra cache trước, nếu hit thì return of(cachedData) bỏ qua handler. Timeout — dùng timeout(5000) throw TimeoutError sau 5 giây. Error mapping — catchError() để transform exceptions.
Apply với @UseInterceptors() ở route/controller hoặc global qua APP_INTERCEPTOR. Khác Guard và Pipe, Interceptors chạy cả trước lẫn sau handler nên có thể transform response.
Exception Filters bắt các exceptions được throw trong ứng dụng và format response lỗi. NestJS có built-in filter xử lý HttpException và các subclass của nó. Nếu exception không phải HttpException, NestJS trả về 500 Internal Server Error mặc định.
Custom global filter implement ExceptionFilter với method catch(exception, host). host.switchToHttp() lấy req/res. Trong filter có thể check instanceof HttpException để lấy status code, log lỗi, format response chuẩn với statusCode, message, timestamp, path.
Built-in HTTP exceptions: NotFoundException, BadRequestException, UnauthorizedException, ForbiddenException, ConflictException, InternalServerErrorException... Đăng ký global qua app.useGlobalFilters() hoặc preferred là { provide: APP_FILTER, useClass: ... } để hỗ trợ DI.
TypeORM là một trong những ORM phổ biến nhất với NestJS (Prisma cũng được ưa chuộng trong các project mới), sử dụng Data Mapper pattern thông qua Repository. Setup với TypeOrmModule.forRoot() trong AppModule cấu hình connection (type, host, port, credentials, entities path), synchronize: false trong production — dùng migrations thay.
Entity định nghĩa bằng @Entity() decorator với các column decorators: @PrimaryGeneratedColumn(), @Column(), @CreateDateColumn(), @UpdateDateColumn(). Relations: @OneToMany(), @ManyToOne(), @ManyToMany(), @OneToOne().
Trong feature module, TypeOrmModule.forFeature([Entity]) đăng ký Repository. Service inject @InjectRepository(Entity) để dùng Repository<Entity> với các methods: find(), findOne({ where: { id } }), create(), save(), update(), delete(). Lưu ý: TypeORM 0.3+ yêu cầu findOne() phải có { where: {...} }. Query Builder cho queries phức tạp: createQueryBuilder('alias').leftJoinAndSelect().where().getMany().
JWT Auth flow gồm hai phần chính: AuthService xác thực credentials và cấp token, JwtStrategy/Guard bảo vệ routes. Cài đặt @nestjs/jwt, @nestjs/passport, passport, passport-jwt, bcryptjs. Tạo AuthModule import JwtModule.registerAsync() với ConfigService để lấy JWT_SECRET và expiresIn.
AuthService có method login(): tìm user theo email, compare password với bcrypt.compare(), nếu đúng sign JWT với payload { sub: userId, email, role } và trả về access_token (ngắn hạn 15m) + refresh_token (dài hạn 7d).
JwtStrategy extends PassportStrategy(Strategy) với config jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken() và secretOrKey. Method validate(payload) attach user vào request. Guard JwtAuthGuard extends AuthGuard('jwt') từ Passport. @Public() decorator dùng SetMetadata để bỏ qua guard cho login/register endpoints.
NestJS dùng Jest và cung cấp Test.createTestingModule() để tạo test environment với DI container đầy đủ.
Unit test service: tạo mock repository với jest.fn() cho mỗi method, đăng ký trong module với { provide: getRepositoryToken(Entity), useValue: mockRepo }. Test từng method riêng lẻ, verify calls với expect(mock).toHaveBeenCalledWith(), reset mocks với jest.clearAllMocks() sau mỗi test.
E2E/Integration test: dùng Test.createTestingModule({ imports: [AppModule] }), override providers với .overrideProvider(Service).useValue(mockService), tạo app với moduleFixture.createNestApplication(), init global pipes/guards, dùng supertest để call real HTTP endpoints. Ưu tiên override service thay vì repository cho integration tests để test controller + service logic mà không cần DB thật.
Prisma là ORM thế hệ mới với type-safety tuyệt vời, ngày càng được ưa dùng thay TypeORM. Schema định nghĩa trong prisma/schema.prisma với cú pháp riêng, prisma generate tạo Prisma Client type-safe hoàn toàn.
Setup NestJS: tạo PrismaService extends PrismaClient implements OnModuleInit, gọi this.$connect() trong onModuleInit(). Wrap trong @Global() @Module() để dùng toàn app. Prisma Client API rất fluent: prisma.user.findMany({ include, where, orderBy }), prisma.user.create({ data }), transactions với prisma.$transaction([]).
So sánh Prisma vs TypeORM: Prisma có type-safety tuyệt vời (auto-generated types từ schema), prisma migrate dev rõ ràng an toàn hơn synchronize: true của TypeORM. TypeORM quen thuộc với Java/Spring developers, hỗ trợ Active Record pattern. Prisma không hỗ trợ MongoDB aggregation pipeline tốt bằng Mongoose. Hiện tại Prisma được cộng đồng ưa chuộng hơn cho dự án mới.
NestJS tích hợp Multer qua @nestjs/platform-express để xử lý multipart/form-data. Không cần install thêm gì với Express adapter.
Upload single file: dùng @UseInterceptors(FileInterceptor('fieldName', options)) và @UploadedFile() decorator. Options quan trọng: storage — diskStorage() lưu disk hoặc memoryStorage() lưu buffer (dùng khi upload S3), fileFilter để reject file không hợp lệ, limits.fileSize giới hạn kích thước.
Upload multiple files: FilesInterceptor('fieldName', maxCount) với @UploadedFiles(). ParseFilePipe với validators MaxFileSizeValidator và FileTypeValidator là cách clean nhất để validate. Upload S3: dùng memoryStorage() để lấy file.buffer, gọi AWS SDK s3.putObject() với buffer và file.mimetype. Luôn generate tên file ngẫu nhiên để tránh xung đột và directory traversal.
Custom Decorators giúp code sạch hơn và tái sử dụng được. NestJS cung cấp createParamDecorator cho param decorators và SetMetadata để đính kèm metadata.
@CurrentUser: dùng createParamDecorator((data, ctx) => ctx.switchToHttp().getRequest().user). Có thể nhận tham số để extract field cụ thể: @CurrentUser('email') trả về user.email, @CurrentUser() trả về toàn bộ user object. Dùng trong controller thay vì @Request() req rồi req.user.
@Roles(...roles): dùng SetMetadata('roles', roles) để đính kèm metadata. RolesGuard implement CanActivate, dùng Reflector.getAllAndOverride() để đọc required roles từ handler và class, so sánh với request.user.role.
@Public(): SetMetadata(IS_PUBLIC_KEY, true), đọc trong JwtAuthGuard — nếu isPublic === true thì return true ngay mà không verify token. Pattern này cho phép global guard nhưng vẫn có public routes.
NestJS có 3 provider scopes kiểm soát vòng đời instance:
DEFAULT (Singleton): một instance dùng cho toàn app — đây là default và phổ biến nhất. Phù hợp cho stateless services như DatabaseService, ConfigService.
REQUEST: tạo instance mới cho mỗi incoming request, bị destroy sau khi response xong. Dùng khi service cần lưu request-specific data (tenant context, request ID, user info). Nhược điểm: tốn memory hơn và các dependencies của nó cũng bị kéo thành REQUEST scope.
TRANSIENT: tạo instance mới mỗi lần được inject — không share giữa các consumers. Dùng khi cần isolated state mỗi lần dùng.
Scope injection chain: nếu SERVICE_A (REQUEST scope) được inject vào SERVICE_B, SERVICE_B cũng tự động trở thành REQUEST scope. Pitfall: dùng REQUEST scope tràn lan làm giảm performance đáng kể — chỉ dùng khi thực sự cần.
Dynamic modules cho phép configure module lúc runtime với tham số — khác static modules cấu hình cứng trong code.
forRoot(options) là synchronous factory nhận options và trả về DynamicModule. forRootAsync(options) hỗ trợ async config như đọc từ ConfigService:
// Cách implement forRootAsync trong custom module
static forRootAsync(options: AsyncOptions): DynamicModule {
return {
module: DatabaseModule,
imports: options.imports || [],
providers: [
{
provide: DATABASE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
},
DatabaseService,
],
exports: [DatabaseService],
};
}Dùng trong AppModule:
DatabaseModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
url: config.get('DATABASE_URL'),
}),
inject: [ConfigService],
})Pattern này dùng trong TypeOrmModule.forRootAsync(), JwtModule.registerAsync(), CacheModule.registerAsync().
Custom providers cho phép kiểm soát cách NestJS tạo và inject dependencies:
useValue: inject giá trị cụ thể — thường dùng cho config objects, mocking trong tests:
{ provide: 'CONFIG', useValue: { apiKey: 'abc' } }useClass: chỉ định class khác để inject — dùng để swap implementation (mock, stub):
{ provide: UserService, useClass: MockUserService }useFactory: factory function tạo provider — hỗ trợ async và inject dependencies:
{ provide: 'DB', useFactory: async (config: ConfigService) => {
return createConnection(config.get('DB_URL'))
}, inject: [ConfigService] }useExisting: alias — inject cùng instance từ token khác:
{ provide: 'LOGGER', useExisting: WinstonLogger }Sử dụng string token cần @Inject('TOKEN') decorator trong constructor vì TypeScript không thể reflect string literals.
ExecutionContext extends ArgumentsHost, cung cấp thông tin về execution context hiện tại (HTTP, WebSocket, RPC). Guards, Interceptors và Exception Filters đều nhận ExecutionContext.
Các methods quan trọng:
- getType(): trả về 'http' | 'ws' | 'rpc'
- switchToHttp(): trả về HttpArgumentsHost với getRequest(), getResponse()
- switchToWs(): trả về WsArgumentsHost với getData(), getClient()
- switchToRpc(): cho microservices
- getHandler(): trả về handler function đang được gọi
- getClass(): trả về controller class
Dùng getHandler() và getClass() kết hợp Reflector để đọc metadata:
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(), // Route-level metadata
context.getClass(), // Controller-level metadata
]);Pattern này cho phép metadata được định nghĩa ở cả route lẫn controller, với route-level ưu tiên hơn.
Transactions đảm bảo multiple DB operations thành công hoặc rollback toàn bộ.
Cách 1 — QueryRunner (recommend cho complex transactions):
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.save(User, user);
await queryRunner.manager.save(Profile, profile);
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}Cách 2 — EntityManager.transaction() (cleaner cho simple cases):
await this.dataSource.transaction(async manager => {
await manager.save(User, user);
await manager.save(Profile, profile);
// Tự động rollback nếu throw
});Cách 3 — @Transaction decorator (deprecated trong TypeORM 0.3+, không dùng).
Pitfall: không mix repository từ DI và queryRunner.manager trong cùng transaction — chúng dùng connection pool khác nhau.
Repository API (High-level): phù hợp cho CRUD đơn giản, dễ đọc, type-safe:
const users = await this.usersRepo.find({
where: { isActive: true, role: Role.USER },
relations: ['profile'],
order: { createdAt: 'DESC' },
take: 20, skip: 0,
});Query Builder (Low-level): cho queries phức tạp với dynamic conditions, subqueries, raw SQL expressions:
const result = await this.usersRepo
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.where('user.isActive = :active', { active: true })
.andWhere('post.publishedAt > :date', { date: lastWeek })
.select(['user.id', 'user.email', 'COUNT(post.id) as postCount'])
.groupBy('user.id')
.having('COUNT(post.id) > 0')
.orderBy('postCount', 'DESC')
.getRawMany();Dùng Repository API cho 80% cases.
Dùng Query Builder khi: complex JOINs, aggregations (COUNT/SUM/AVG), dynamic WHERE conditions, raw SQL expressions cần.
Helmet: HTTP security headers middleware — ngăn chặn XSS, clickjacking, sniffing:
import helmet from 'helmet';
app.use(helmet()); // Thêm vào main.tsCORS: chỉ allow origins cụ thể:
app.enableCors({
origin: ['https://yourdomain.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
});Rate Limiting với @nestjs/throttler:
ThrottlerModule.forRoot([
{ name: 'short', ttl: 1000, limit: 3 }, // 3 req/s
{ name: 'medium', ttl: 10000, limit: 20 }, // 20 req/10s
{ name: 'long', ttl: 60000, limit: 100 }, // 100 req/min
])Input sanitization: class-validator + ValidationPipe với whitelist: true ngăn chặn mass assignment.
Dùng sanitize-html cho user-generated content. SQL Injection: TypeORM parameterized queries tự động escape — không bao giờ dùng raw string interpolation trong queries.
JWT (Stateless): token mang đủ thông tin, server không cần lưu state. Phù hợp:
- Microservices và distributed systems
- Mobile apps (localStorage/SecureStorage)
- Stateless REST APIs
- Cross-domain authentication
Sessions (Stateful): server lưu session data (DB hoặc Redis), client chỉ giữ session ID trong cookie. Phù hợp:
- Traditional web apps với server-side rendering
- Cần revoke session ngay lập tức (banking, admin)
- Không muốn expose user data trong token
NestJS Session setup với express-session + Redis:
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, maxAge: 86400000 },
}));Pitfall JWT: không thể revoke trước khi hết hạn trừ khi maintain blacklist (làm mất đi lợi thế stateless).
Pitfall Session: cần sticky sessions hoặc centralized store (Redis) khi scale horizontally.
Unit tests trong NestJS isolate một class bằng cách mock tất cả dependencies.
Pattern chuẩn với Test.createTestingModule():
describe('UsersService', () => {
let service: UsersService;
let repo: jest.Mocked<Repository<User>>;
beforeEach(async () => {
const mockRepo = {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepo },
],
}).compile();
service = module.get<UsersService>(UsersService);
repo = module.get(getRepositoryToken(User));
});
it('should find user by id', async () => {
const user = { id: 1, email: 'test@test.com' };
repo.findOne.mockResolvedValue(user);
const result = await service.findOne(1);
expect(result).toEqual(user);
expect(repo.findOne).toHaveBeenCalledWith({ where: { id: 1 } });
});
});Dùng jest.spyOn() để spy mà không replace hoàn toàn.
Dùng jest.clearAllMocks() trong afterEach để reset state.
E2E tests test toàn bộ HTTP flow từ request đến response mà không cần real external services.
// test/users.e2e-spec.ts
describe('Users (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(UsersService) // Override real service
.useValue(mockUsersService)
.compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
app.useGlobalFilters(new HttpExceptionFilter());
await app.init();
});
afterAll(async () => {
await app.close();
});
it('GET /users — returns users list', () => {
return request(app.getHttpServer())
.get('/users')
.set('Authorization', `Bearer ${testToken}`)
.expect(200)
.expect(res => {
expect(res.body.data).toBeInstanceOf(Array);
});
});
});Best practices: dùng test database thực (SQLite in-memory hoặc test Postgres), seed data trong beforeAll, cleanup trong afterAll.
Chạy E2E riêng biệt với jest --testPathPattern=e2e.
NestJS cung cấp @nestjs/event-emitter (wrapper của EventEmitter2) cho internal events — không phải distributed messaging mà là in-process pub/sub.
// Setup
EventEmitterModule.forRoot({ wildcard: true, delimiter: '.' })
// Emit từ service
import { EventEmitter2 } from '@nestjs/event-emitter';
this.eventEmitter.emit('order.created', new OrderCreatedEvent(order));
// Listen với @OnEvent
@OnEvent('order.created')
async handleOrderCreated(event: OrderCreatedEvent) {
await this.emailService.sendOrderConfirmation(event.order);
}
// Wildcard
@OnEvent('order.*') // Bắt tất cả order events
async handleAllOrderEvents(event: any) { ... }Async events: @OnEvent('order.created', { async: true }) để handler chạy async không block emitter.
Dùng cho: decoupling business logic (sau khi create order, nhiều services cần xử lý), audit logging, notifications. Pitfall: không dùng cho cross-service communication — dùng Kafka/RabbitMQ thay.
Offset-based (skip/take): đơn giản, hỗ trợ random page access:
async findAll(page: number, limit: number) {
const [data, total] = await this.repo.findAndCount({
take: limit, skip: (page - 1) * limit,
order: { createdAt: 'DESC' },
});
return { data, total, page, lastPage: Math.ceil(total / limit) };
}Nhược điểm: không ổn định khi data thay đổi real-time, chậm khi skip lớn (DB phải scan).
Cursor-based (keyset pagination): ổn định hơn cho real-time feeds, hiệu quả hơn khi scale:
async findAfterCursor(cursor: string, limit: number) {
const decodedCursor = Buffer.from(cursor, 'base64').toString();
// cursor = ISO timestamp của item cuối cùng
const data = await this.repo.find({
where: { createdAt: LessThan(new Date(decodedCursor)) },
take: limit + 1, // +1 để biết có page tiếp không
order: { createdAt: 'DESC' },
});
const hasMore = data.length > limit;
const items = hasMore ? data.slice(0, -1) : data;
const nextCursor = hasMore ? Buffer.from(items.at(-1)!.createdAt.toISOString()).toString('base64') : null;
return { items, nextCursor, hasMore };
}Dùng offset cho: admin dashboards, search results.
Dùng cursor cho: social feeds, infinite scroll.
class-transformer cùng với ClassSerializerInterceptor tự động serialize/exclude fields trong response.
Exclude sensitive fields (password, tokens):
import { Exclude, Expose, Transform } from 'class-transformer';
export class UserEntity {
id: number;
email: string;
@Exclude() // Không expose trong response
password: string;
@Expose()
@Transform(({ value }) => value?.toISOString())
createdAt: Date;
constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial);
}
}Enable globally:
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));Controller return entity:
@Get(':id')
async findOne(@Param('id') id: number): Promise<UserEntity> {
const user = await this.usersService.findOne(id);
return new UserEntity(user); // Wrap trong entity class
}Pitfall: plain objects không bị transform — phải trả về instance của entity class để decorator có effect.
TypeORM cung cấp decorators tiện lợi cho timestamps và soft delete:
import {
CreateDateColumn, UpdateDateColumn, DeleteDateColumn,
PrimaryGeneratedColumn, Column, Entity,
} from 'typeorm';
@Entity()
export class BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@CreateDateColumn() // Tự set khi INSERT
createdAt: Date;
@UpdateDateColumn() // Tự update khi UPDATE
updatedAt: Date;
@DeleteDateColumn() // Tự set khi softDelete(), NULL khi active
deletedAt: Date | null;
}Soft delete với TypeORM:
// Soft delete — set deletedAt
await this.repo.softDelete(id);
// Restore
await this.repo.restore(id);
// find() tự động filter deletedAt IS NULL
// Để include deleted:
await this.repo.find({ withDeleted: true });Pitfall: @DeleteDateColumn chỉ hoạt động khi dùng softDelete() và softRemove() — không phải delete() hay remove().
Connection pooling và query optimization là hai yếu tố quan trọng nhất để NestJS app chịu tải tốt ở production.
Connection Pooling với TypeORM:
TypeOrmModule.forRootAsync({
useFactory: (config: ConfigService) => ({
type: 'postgres',
url: config.get('DATABASE_URL'),
// Connection pool settings
extra: {
max: 20, // Max connections (CPU cores * 2-4 là rule of thumb)
min: 2, // Min connections luôn mở
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
},
logging: config.get('NODE_ENV') === 'development',
}),
})Query optimization:
1. Dùng select để chỉ lấy columns cần thiết
2. Tạo indexes trên foreign keys và frequently queried columns
3. Dùng QueryBuilder với addSelect thay vì relations cho large datasets
4. Paginate tất cả list queries — không bao giờ findAll() không có limit
5. Slow query logging: enable logging: ['query', 'slow'] với maxQueryExecutionTime: 1000
DataSource injection (TypeORM 0.3+):
constructor(private readonly dataSource: DataSource) {}
// Dùng this.dataSource.createQueryRunner() cho transactionsNestJS Microservices là pattern giao tiếp giữa services dùng message-passing thay vì HTTP. NestFactory.createMicroservice() tạo microservice với transport layer được chọn.
Transport layers: TCP cho internal services latency thấp, Redis cho Pub/Sub và simple messaging, NATS cho lightweight cloud-native messaging, Kafka cho high-throughput event streaming, RabbitMQ cho complex routing với dead-letter queues, gRPC cho strongly-typed high-performance calls.
Hai pattern messaging: @MessagePattern('event') cho Request/Response (sender chờ reply), @EventPattern('event') cho Fire-and-forget (không chờ reply). API Gateway dùng ClientsModule.register() inject ClientProxy, gọi client.send('pattern', data) cho request/response trả về Observable, client.emit('pattern', data) cho fire-and-forget. Hybrid app có thể serve cả HTTP và Microservices cùng lúc với app.connectMicroservice().
NestJS hỗ trợ WebSockets qua @WebSocketGateway() decorator, tích hợp với socket.io hoặc ws. Gateway là class đặc biệt giống Controller nhưng xử lý WebSocket events.
Gateway implement 3 interfaces: OnGatewayInit (sau khi khởi tạo), OnGatewayConnection (client connect), OnGatewayDisconnect (client disconnect). @WebSocketServer() inject server instance. @SubscribeMessage('event') handle event từ client, tương tự @Get() trong controller. @MessageBody() và @ConnectedSocket() là param decorators cho WS.
Broadcast: this.server.emit() gửi cho tất cả, this.server.to(room).emit() gửi cho room, client.to(room).emit() gửi cho room trừ sender. Guards và Interceptors hoạt động với WS Gateway giống HTTP. Namespace với namespace: '/chat' trong decorator options để tách biệt connections.
Fastify adapter thay Express: dùng FastifyAdapter từ @nestjs/platform-fastify, nhanh hơn 2-3x nhờ JSON serialization tối ưu và request/response lifecycle hiệu quả hơn. Nhưng một số middleware Express không tương thích, cần kiểm tra.
Caching: @nestjs/cache-manager với Redis store qua CacheModule.registerAsync(). @UseInterceptors(CacheInterceptor) tự động cache GET responses, @CacheTTL(seconds) override TTL cho route cụ thể. Programmatic cache với CacheManager.get/set/del.
Compression: app.use(compression()) giảm response size 60-70% cho text content.
Rate Limiting: @nestjs/throttler với ThrottlerModule.forRoot() config nhiều tiers (short/long). APP_GUARD với ThrottlerGuard apply global, @Throttle() override cho route cụ thể, @SkipThrottle() bỏ qua.
DB connection pooling: TypeORM extra.max tăng max connections theo CPU cores. Lazy loading modules với LazyModuleLoader cho microservices để giảm startup time.
Middleware thực thi trước Guards, có access vào raw req/res/next giống Express. Dùng cho logging, CORS, session, body parsing, request tracking.
Functional middleware là function đơn giản (req, res, next) => { next() } — dùng khi không cần DI. Class middleware implement NestMiddleware với method use(req, res, next) và @Injectable() — dùng khi cần inject services.
Apply middleware trong module implements NestModule với configure(consumer: MiddlewareConsumer). consumer.apply(Middleware).forRoutes('*') apply cho tất cả. .forRoutes({ path: 'users', method: RequestMethod.GET }) hoặc .forRoutes(UsersController) cho specific routes/controllers. .exclude() loại trừ routes cụ thể. Có thể chain nhiều middleware: .apply(M1, M2, M3). Global middleware (Express-style) dùng app.use(helmet(), cors(), express.json()) trong main.ts trước khi listen.
NestJS DI container theo hierarchy: Global providers (APP_GUARD, APP_PIPE...) → Module providers → Controller providers. Khi inject một dependency, NestJS tìm trong:
1. Module hiện tại
2. Imported modules (providers được export)
3. Global modules
Global providers khai báo qua @Global() module hoặc useGlobal*() — available khắp nơi không cần import.
Module-scoped providers chỉ visible trong module đó và các module import nó. Phải export provider mới có thể dùng ngoài module.
Shared Module pattern: tạo SharedModule export các providers dùng chung (PrismaService, ConfigService), import vào các feature modules cần dùng. Tốt hơn @Global() vì dependency explicit.
Pitfall: circular module imports (ModuleA imports ModuleB và ngược lại) — giải quyết bằng forwardRef(() => ModuleB) trong imports array.
RBAC cho phép kiểm soát quyền truy cập dựa trên role của user. Pattern chuẩn:
Bước 1: tạo @Roles() decorator:
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);Bước 2: tạo RolesGuard:
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const required = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
ctx.getHandler(), ctx.getClass(),
]);
if (!required) return true; // No roles required
const { user } = ctx.switchToHttp().getRequest();
return required.some(role => user.roles?.includes(role));
}
}Bước 3: đăng ký global và dùng:
@Roles(Role.ADMIN)
@Get('admin-only')
getAdminData() { ... }Ngoài roles, có thể implement Permission-based (fine-grained) với casl library: ability.can(Action.Update, 'User').
Pitfall: guard chạy sau JWT guard — đảm bảo user object đã được attach vào request trước khi roles guard chạy.
TypeORM migrations quản lý schema changes an toàn. Không dùng synchronize: true trong production — có thể làm mất data.
Workflow chuẩn:
# 1. Generate migration từ entity changes
npx typeorm migration:generate -d src/data-source.ts src/migrations/AddUserPhone
# 2. Review file migration được tạo
# src/migrations/1234567890-AddUserPhone.ts
# 3. Apply
npx typeorm migration:run -d src/data-source.ts
# 4. Revert nếu cần
npx typeorm migration:revert -d src/data-source.tsData Source file cần tách biệt với app module để CLI có thể dùng:
// src/data-source.ts
export const AppDataSource = new DataSource({
type: 'postgres',
entities: ['src/**/*.entity.ts'],
migrations: ['src/migrations/*.ts'],
synchronize: false,
});Best practices: chạy migration tự động khi app start trong production (runMigrations: true), never drop column directly — add new column, migrate data, then drop old.
Luôn test migration revert trước khi deploy.
Access token ngắn hạn (15m) + refresh token dài hạn (7d) là pattern chuẩn.
Flow: login → trả access + refresh token → access token hết hạn → dùng refresh token để lấy access token mới → rotate refresh token.
Refresh Token Rotation: mỗi lần refresh, invalidate token cũ và cấp token mới — detect token theft:
async refresh(refreshToken: string) {
const payload = this.jwtService.verify(refreshToken, { secret: REFRESH_SECRET });
const user = await this.usersService.findOne(payload.sub);
// Validate token khớp với stored (hashed) token
const isValid = await bcrypt.compare(refreshToken, user.hashedRefreshToken);
if (!isValid) throw new ForbiddenException('Token reuse detected');
// Rotate — tạo tokens mới, hash và lưu token mới
const tokens = await this.getTokens(user.id, user.email);
await this.updateRefreshToken(user.id, tokens.refreshToken);
return tokens;
}Revocation: lưu refresh token (bcrypt hash) trong DB, khi logout gọi updateRefreshToken(userId, null).
Pitfall: không lưu plain refresh token trong DB — luôn hash.
Kafka phù hợp cho high-throughput event streaming. NestJS có built-in Kafka transport.
Consumer (Microservice):
// main.ts
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.KAFKA,
options: {
client: { brokers: ['kafka:9092'] },
consumer: { groupId: 'orders-consumer' },
},
});
// Controller
@EventPattern('order.created')
async handleOrderCreated(@Payload() data: OrderCreatedEvent) {
await this.ordersService.process(data);
}Producer (API Gateway):
ClientsModule.register([{
name: 'KAFKA_SERVICE',
transport: Transport.KAFKA,
options: {
client: { clientId: 'api-gateway', brokers: ['kafka:9092'] },
producer: { allowAutoTopicCreation: true },
},
}])
// Service
this.kafkaClient.emit('order.created', { orderId, userId, items });Kafka patterns: @EventPattern cho pub/sub (fire-and-forget), @MessagePattern cho request-reply.
Dùng Avro schema registry cho type-safe messages trong production.
Hybrid app cho phép một NestJS app expose cả HTTP REST endpoints lẫn microservice message handlers — hữu ích khi muốn API Gateway cũng lắng nghe events.
// main.ts
const app = await NestFactory.create(AppModule);
// Attach microservice
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.TCP,
options: { host: '0.0.0.0', port: 3001 },
});
// Start cả hai
await app.startAllMicroservices(); // Phải gọi trước listen
await app.listen(3000);Controller vừa có @Get() cho HTTP vừa có @MessagePattern() cho RPC:
@Controller('orders')
export class OrdersController {
@Get()
async findAll() { ... } // HTTP
@MessagePattern({ cmd: 'get_orders' })
async findAllRpc(@Payload() data: any) { ... } // TCP/RPC
}Use case: API Gateway nhận HTTP từ frontend, đồng thời lắng nghe events từ internal services qua Kafka/TCP để cập nhật cache.
Logging chuẩn production cần: structured JSON, log levels, request correlation ID, không log sensitive data.
Setup Winston với nest-winston:
import { WinstonModule } from 'nest-winston';
import { transports, format } from 'winston';
WinstonModule.forRoot({
level: process.env.LOG_LEVEL || 'info',
format: format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.json(), // Structured JSON cho log aggregation
),
transports: [
new transports.Console(),
new transports.File({ filename: 'error.log', level: 'error' }),
],
})Request correlation với Middleware:
app.use((req, res, next) => {
req.correlationId = req.headers['x-correlation-id'] ?? uuid();
res.setHeader('x-correlation-id', req.correlationId);
next();
});Inject Logger service và dùng this.logger.log/error/warn với context.
Pitfall: không dùng console.log trong production code — không structured, không có levels.
Health Checks với @nestjs/terminus:
import { TerminusModule, TypeOrmHealthIndicator, HealthCheckService } from '@nestjs/terminus';
// Lưu ý: HealthController là controller do user tạo, không phải từ @nestjs/terminus.
@Module({ imports: [TerminusModule] })
export class HealthModule {}
// controller
@Get('health')
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('database'),
() => this.http.pingCheck('redis', 'http://redis:6379'),
() => this.memory.checkHeap('memory_heap', 300 * 1024 * 1024),
]);
}Graceful Shutdown: đảm bảo app xử lý xong in-flight requests trước khi shutdown:
// main.ts
app.enableShutdownHooks(); // Listen SIGTERM, SIGINT
// Service có thể implement OnApplicationShutdown
async onApplicationShutdown(signal?: string) {
this.logger.log(`Shutting down on signal: ${signal}`);
await this.closeConnections();
}Kubernetes gửi SIGTERM khi pod bị terminate — NestJS cần hoàn thành requests đang xử lý trước khi exit.
Set terminationGracePeriodSeconds: 30 trong K8s manifest.
API Key Auth Guard: extract key từ header, validate trong DB hoặc cache:
@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(private apiKeysService: ApiKeysService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const apiKey = request.headers['x-api-key'];
if (!apiKey) throw new UnauthorizedException('API key required');
const keyRecord = await this.apiKeysService.validateKey(apiKey);
if (!keyRecord) throw new UnauthorizedException('Invalid API key');
request.tenant = keyRecord.tenant; // Attach tenant context
return true;
}
}Multi-tenant pattern: mỗi request mang tenant context, services filter data theo tenant:
// REQUEST scope — instance riêng mỗi request
@Injectable({ scope: Scope.REQUEST })
export class TenantContext {
constructor(@Inject(REQUEST) private req: Request) {}
get tenantId(): string {
return (this.req as any).tenant?.id;
}
}Các strategies: separate DB per tenant, shared DB với tenant_id column, schema per tenant.
Cân bằng giữa isolation và cost.
Setup với @nestjs/cache-manager và Redis:
CacheModule.registerAsync({
isGlobal: true,
useFactory: async (config: ConfigService) => ({
store: await redisStore({ socket: { host: config.get('REDIS_HOST'), port: 6379 } }),
ttl: 60 * 1000, // 60 giây default
}),
inject: [ConfigService],
})Automatic caching với interceptor:
@UseInterceptors(CacheInterceptor)
@CacheTTL(300) // NestJS decorator dùng SECONDS (300s = 5 phút)
@Get('products')
async getProducts() { ... }Manual caching (more control):
constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}
async getProduct(id: number) {
const cached = await this.cache.get<Product>(`product:${id}`);
if (cached) return cached;
const product = await this.repo.findOne({ where: { id } });
await this.cache.set(`product:${id}`, product, 300000); // cache-manager v5 dùng MILLISECONDS
return product;
}
async updateProduct(id: number, dto: UpdateProductDto) {
const product = await this.repo.save({ id, ...dto });
await this.cache.del(`product:${id}`); // Invalidate
return product;
}Cache-aside pattern: luôn invalidate khi write, set TTL ngắn hơn thực tế cần để tránh stale data quá lâu.
gRPC là high-performance RPC framework dùng Protocol Buffers (protobuf) thay vì JSON — phù hợp cho internal microservice communication.
Setup:
// main.ts — gRPC microservice
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.GRPC,
options: {
package: 'users',
protoPath: join(__dirname, 'users.proto'),
url: '0.0.0.0:5000',
},
});
// Controller
@GrpcMethod('UsersService', 'FindOne')
findOne(data: { id: number }): UserMessage {
return this.usersService.findOne(data.id);
}proto file:
service UsersService {
rpc FindOne (FindOneRequest) returns (User);
rpc FindAll (Empty) returns (UsersResponse);
}So sánh gRPC vs REST:
- gRPC: binary (nhỏ hơn 3-10x), strongly typed, bidirectional streaming, HTTP/2
- REST: text-based JSON, human-readable, universal browser support, simpler
Dùng gRPC cho: internal microservice-to-microservice communication có throughput cao. Dùng REST cho: public APIs, browser clients.
@nestjs/schedule wrapper của node-cron cho phép chạy tasks theo lịch:
import { Cron, CronExpression, Interval, Timeout } from '@nestjs/schedule';
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
// Chạy lúc 12:00 AM mỗi ngày
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async generateDailyReport() {
this.logger.log('Generating daily report...');
await this.reportsService.generate();
}
// Chạy mỗi 30 giây
@Interval(30000)
async syncExternalData() { ... }
// Chạy một lần sau 5 giây kể từ khi app start
@Timeout(5000)
async warmupCache() { ... }
// Dynamic cron với SchedulerRegistry
constructor(private schedulerRegistry: SchedulerRegistry) {}
addCronJob(name: string, cronTime: string) {
const job = new CronJob(cronTime, () => this.doWork());
this.schedulerRegistry.addCronJob(name, job);
job.start();
}
}Pitfall: trong distributed/multiple instances, cần distributed lock (Redis + Redlock) để tránh cùng job chạy parallel trên nhiều pods.
Nested DTOs với @ValidateNested() và @Type():
import { ValidateNested, IsArray, ArrayMinSize } from 'class-validator';
import { Type } from 'class-transformer';
class AddressDto {
@IsString() @Length(2, 100)
street: string;
@IsString() @IsPostalCode('VN')
zipCode: string;
}
class CreateUserDto {
@IsString() @MinLength(2)
name: string;
@ValidateNested() // Validate nested object
@Type(() => AddressDto) // Phải có @Type để transform
address: AddressDto;
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true }) // Validate từng item
@Type(() => AddressDto)
addresses: AddressDto[];
}Conditional validation với @ValidateIf():
@IsOptional()
paymentMethod?: string;
@ValidateIf(obj => obj.paymentMethod === 'CREDIT_CARD')
@IsCreditCard()
cardNumber?: string; // Chỉ validate nếu paymentMethod là CREDIT_CARDPitfall: @Type(() => NestedClass) là bắt buộc cho @ValidateNested() — thiếu thì validation bỏ qua nested object.
Multi-stage Dockerfile để minimize production image size:
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
# Chỉ copy prod dependencies
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json .
# Non-root user
RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001
USER nestjs
EXPOSE 3000
CMD ["node", "dist/main.js"]docker-compose cho development:
services:
api:
build: .
ports: ['3000:3000']
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/mydb
depends_on: [db, redis]
db:
image: postgres:16-alpine
volumes: ['pgdata:/var/lib/postgresql/data']Best practices: dùng non-root user, .dockerignore exclude node_modules/dist, health check trong Dockerfile.
Error handling trong microservices khác với HTTP — không có response để throw exception trực tiếp.
TCP/Redis transport — throw RpcException:
@MessagePattern({ cmd: 'get_user' })
async getUser(@Payload() data: { id: number }) {
const user = await this.usersService.findOne(data.id);
if (!user) throw new RpcException({ status: 404, message: 'User not found' });
return user;
}
// Client side — catch error (dùng lastValueFrom thay .toPromise() deprecated trong RxJS 7+)
import { lastValueFrom } from 'rxjs';
await lastValueFrom(
this.client.send<User>({ cmd: 'get_user' }, { id })
.pipe(catchError(err => throwError(() => new NotFoundException(err.message))))
);Dead Letter Queue (DLQ) với RabbitMQ:
// Setup DLQ binding trong RabbitMQ management
// Messages fail → retry queue → DLQ sau N retries
ClientsModule.register([{
name: 'RABBIT_SERVICE',
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
queue: 'orders_queue',
queueOptions: {
durable: true,
deadLetterExchange: 'dlx', // Failed messages go here
messageTtl: 10000,
},
},
}])Monitoring: log failed messages với correlation ID, alert khi DLQ depth tăng, implement manual replay từ DLQ.