NestJS #
NestJS adalah framework backend Node.js yang menggabungkan konsep Object-Oriented Programming, Functional Programming, dan Reactive Programming dalam satu arsitektur terstruktur. Berangkat dari filosofi “Angular untuk backend”, NestJS memperkenalkan module system, decorator, dan dependency injection ke dunia Node.js — tiga pilar yang familiar bagi developer Spring, Laravel, atau .NET, namun tetap ringan dan idiomatik di TypeScript.
Untuk local development, NestJS punya tantangan unik: codebase-nya besar, dependency-nya banyak (TypeScript, ts-node, Nest CLI, reflector metadata, RxJS), dan hot reload harus bisa di-handle di dalam container tanpa kehilangan state TypeScript. Docker adalah jawaban paling solid — kamu menyamakan toolchain, dependensi, dan service pendukung dalam satu stack yang bisa dijalankan ulang dengan satu perintah.
Artikel ini membahas setup Docker Compose untuk local development NestJS secara mendalam: Dockerfile multi-stage, hot reload via nest start --watch, struktur module, controller, service, DTO, validation pipe, dependency injection, TypeORM dengan Postgres, dan best practice production-grade.
Prasyarat #
Pastikan sudah terinstall di host:
- Docker dan Docker Compose versi terbaru (v2.20+)
- Node.js 20+ — untuk tooling di host (opsional, hanya jika ingin menjalankan Nest CLI tanpa Docker)
- Nest CLI —
npm i -g @nestjs/cli(opsional, untuk scaffolding manual)
Struktur project NestJS yang diasumsikan:
nestjs-app/
├── src/
│ ├── main.ts
│ ├── app.module.ts
│ ├── common/
│ │ ├── filters/
│ │ ├── interceptors/
│ │ ├── guards/
│ │ └── pipes/
│ ├── config/
│ │ ├── database.config.ts
│ │ └── validation.schema.ts
│ ├── users/
│ │ ├── users.module.ts
│ │ ├── users.controller.ts
│ │ ├── users.service.ts
│ │ ├── dto/
│ │ │ ├── create-user.dto.ts
│ │ │ └── update-user.dto.ts
│ │ └── entities/
│ │ └── user.entity.ts
│ └── health/
│ └── health.controller.ts
├── test/
├── docker-compose.yml
├── Dockerfile
├── .dockerignore
├── .env
├── nest-cli.json
├── package.json
├── package-lock.json
└── tsconfig.json
NestJS punya CLI yang powerful:nest new,nest g module users,nest g controller users,nest g service users. CLI ini meng-generate file dengan boilerplate yang sudah benar dan meng-update module secara otomatis. Pakai CLI untuk project baru, edit manual untuk project existing.
Dockerfile Multi-Stage #
NestJS adalah TypeScript — source code perlu dicompile ke JavaScript sebelum dijalankan. Untuk development, kita jalankan langsung via ts-node-dev atau nest start --watch (lebih direkomendasikan). Untuk production, kita compile ke dist/. Dockerfile di bawah memisahkan keduanya.
# syntax=docker/dockerfile:1.6
FROM node:20-alpine AS deps
WORKDIR /app
# Salin manifest lebih dulu untuk cache layer
COPY package.json package-lock.json ./
# Install semua dependency (termasuk devDependencies)
RUN npm install --no-audit --no-fund
# Salin konfigurasi TypeScript dan Nest CLI
COPY tsconfig.json tsconfig.build.json nest-cli.json ./
# Salin source code
COPY src ./src
# ----- Production builder -----
FROM deps AS builder
RUN npm run build
# ----- Production runner -----
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production \
PORT=3000
RUN addgroup -S app && adduser -S app -G app
# Salin hanya dependency production
COPY package.json package-lock.json ./
RUN npm install --omit=dev --no-audit --no-fund
# Salin hasil build
COPY --from=builder --chown=app:app /app/dist ./dist
USER app
EXPOSE 3000
CMD ["node", "dist/main.js"]
Penjelasan Tahapan #
Stage deps — pondasi development. Meng-install semua dependency (termasuk TypeScript, Nest CLI, ts-node) dan menyalin source code. Tahap ini yang dipakai untuk docker compose up harian.
Stage builder — kompilasi TypeScript ke dist/. Berguna untuk test image production lokal atau CI.
Stage runner — image ramping. Hanya menyertakan dist/ dan dependency production. User non-root untuk keamanan.
Kenapa multi-stage? Local dev pakai stage deps (punya hot reload), production build dan test bisa pakai stage builder atau runner tanpa mengubah Dockerfile.
Jangan copynode_modulesdari stagedepske stagerunner. Stagedepsberisi devDependencies yang tidak boleh ada di production. Selalu jalankannpm install --omit=devdi stagerunneragar dependency yang ter-install hanya yang ada didependenciesdipackage.json.
docker-compose.yml untuk Development #
# docker-compose.yml
services:
api:
build:
context: .
dockerfile: Dockerfile
target: deps
image: nestjs-app:dev
container_name: nestjs-dev
command: npm run start:dev
working_dir: /app
ports:
- "3000:3000"
volumes:
- ./src:/app/src
- ./test:/app/test
- /app/node_modules # anonymous volume
- /app/dist # anonymous volume untuk output build
environment:
NODE_ENV: development
PORT: 3000
DATABASE_HOST: db
DATABASE_PORT: 5432
DATABASE_USER: app
DATABASE_PASSWORD: dev
DATABASE_NAME: nestjs_dev
REDIS_HOST: cache
REDIS_PORT: 6379
JWT_SECRET: dev-secret-change-me
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
db:
image: postgres:16-alpine
container_name: nestjs-db
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: dev
POSTGRES_DB: nestjs_dev
volumes:
- db-data:/var/lib/postgresql/data
- ./db/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d nestjs_dev"]
interval: 10s
timeout: 5s
retries: 5
ports:
- "5432:5432"
cache:
image: redis:7-alpine
container_name: nestjs-cache
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
ports:
- "6379:6379"
volumes:
db-data:
Penjelasan Service #
api — service NestJS. target: deps memilih stage development. command: npm run start:dev menjalankan script start:dev di package.json yang kita set ke nest start --watch. Watch mode memantau perubahan file di src/ dan restart server secara otomatis.
Dua anonymous volume untuk node_modules dan dist — keduanya wajib:
/app/node_modulesmencegah host menimpa dependency/app/distmencegahdist/hasil build lokal menumpang di container (yang bisa konflik dengan watch mode)
db dan cache — service pendukung dengan healthcheck. NestJS app tidak akan start sampai Postgres dan Redis benar-benar siap.
Diagram Arsitektur #
flowchart TB
subgraph Host
Dev[Developer]
Code[Source code<br/>src/*.ts]
end
subgraph Docker
App[Container: api<br/>NestJS + watch mode]
DB[(Postgres 16)]
Cache[(Redis 7)]
end
Dev -->|edit .ts file| Code
Code -->|bind mount| App
App -->|query SQL| DB
App -->|cache session/rate limit| Cache
App -.->|healthcheck| DB
App -.->|healthcheck| Cache
Hot Reload dengan nest start –watch #
NestJS punya watch mode bawaan. Sejak Nest CLI versi 9, watch mode menggunakan tsc watch yang memantau perubahan TypeScript dan re-compile, lalu restart server.
package.json:
{
"name": "nestjs-app",
"version": "1.0.0",
"scripts": {
"build": "nest build",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug 0.0.0.0:9229 --watch",
"start:prod": "node dist/main",
"test": "jest",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^10.3.10",
"@nestjs/core": "^10.3.10",
"@nestjs/platform-express": "^10.3.10",
"@nestjs/config": "^3.2.3",
"@nestjs/typeorm": "^10.0.2",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"typeorm": "^0.3.20",
"pg": "^8.12.0",
"class-validator": "^0.14.1",
"class-transformer": "^0.5.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"ioredis": "^5.4.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^10.4.4",
"@nestjs/testing": "^10.3.10",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.12",
"jest": "^29.7.0",
"supertest": "^7.0.0",
"ts-jest": "^29.2.2",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
}
}
nest-cli.json:
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
tsconfig.json:
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"target": "ES2022",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"resolveJsonModule": true
}
}
emitDecoratorMetadata: truedanexperimentalDecorators: truewajib untuk NestJS. Decorator di NestJS tidak bekerja tanpa dua opsi ini. Saat membuat project dengannest new, CLI sudah men-sertakan dua opsi ini secara default.
.dockerignore:
node_modules
dist
coverage
.git
.gitignore
.env
.env.local
Dockerfile
docker-compose.yml
.dockerignore
README.md
*.md
test
Arsitektur NestJS — Module, Controller, Service #
NestJS punya tiga konsep inti yang membedakan dari framework Node.js lain: Module, Controller, dan Service — ditambah Dependency Injection yang mengikat ketiganya.
Module #
Module adalah unit organisasi. Setiap module mengelompokkan controller, service, dan provider lain yang punya kohesi tinggi. Root module (AppModule) mengimpor semua module lain.
src/app.module.ts:
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import { HealthModule } from './health/health.module';
@Module({
imports: [
// ConfigModule global, baca dari .env
ConfigModule.forRoot({
isGlobal: true,
cache: true,
}),
// TypeORM async config — baca dari ConfigService
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.get<string>('DATABASE_HOST'),
port: config.get<number>('DATABASE_PORT'),
username: config.get<string>('DATABASE_USER'),
password: config.get<string>('DATABASE_PASSWORD'),
database: config.get<string>('DATABASE_NAME'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: config.get<string>('NODE_ENV') === 'development',
logging: ['error', 'warn'],
}),
}),
UsersModule,
HealthModule,
],
})
export class AppModule {}
Controller #
Controller adalah layer yang menerima HTTP request, validasi input, dan mengembalikan response. Decorator @Controller, @Get, @Post dideklarasikan di atas class dan method.
src/users/users.controller.ts:
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
UseGuards,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
@Controller('api/v1/users')
@UseGuards(JwtAuthGuard) // semua endpoint butuh auth
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
async findAll(
@Query('limit') limit = 20,
@Query('offset') offset = 0,
) {
return this.usersService.findAll({ limit: +limit, offset: +offset });
}
@Get(':id')
async findOne(@Param('id') id: string) {
const user = await this.usersService.findOne(+id);
return { data: user };
}
@Post()
@HttpCode(HttpStatus.CREATED)
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
async create(@Body() createUserDto: CreateUserDto) {
const user = await this.usersService.create(createUserDto);
return { data: user };
}
@Put(':id')
async update(
@Param('id') id: string,
@Body() updateUserDto: UpdateUserDto,
) {
const updated = await this.usersService.update(+id, updateUserDto);
return { data: updated };
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id') id: string) {
await this.usersService.remove(+id);
}
}
Service #
Service berisi business logic. NestJS inject service ke controller lewat constructor — itulah Dependency Injection-nya.
src/users/users.service.ts:
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async findAll(options: { limit: number; offset: number }) {
const [data, total] = await this.userRepository.findAndCount({
take: options.limit,
skip: options.offset,
order: { id: 'ASC' },
});
return { data, total };
}
async findOne(id: number): Promise<User> {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`User dengan ID ${id} tidak ditemukan`);
}
return user;
}
async create(createUserDto: CreateUserDto): Promise<User> {
const existing = await this.userRepository.findOne({
where: { email: createUserDto.email },
});
if (existing) {
throw new ConflictException('Email sudah terdaftar');
}
const user = this.userRepository.create(createUserDto);
return this.userRepository.save(user);
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.findOne(id); // lempar 404 jika tidak ada
Object.assign(user, updateUserDto);
return this.userRepository.save(user);
}
async remove(id: number): Promise<void> {
const user = await this.findOne(id);
await this.userRepository.remove(user);
}
}
Entity dan DTO #
src/users/entities/user.entity.ts — entity TypeORM:
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
} from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Index({ unique: true })
@Column({ type: 'varchar', length: 255 })
email: string;
@Column({ type: 'varchar', length: 100 })
name: string;
@Column({ type: 'varchar', length: 255 })
passwordHash: string;
@Column({ type: 'varchar', length: 20, default: 'user' })
role: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
src/users/dto/create-user.dto.ts — DTO dengan class-validator:
import {
IsEmail,
IsString,
MinLength,
MaxLength,
IsOptional,
IsEnum,
} from 'class-validator';
import { Transform } from 'class-transformer';
export class CreateUserDto {
@IsEmail()
@Transform(({ value }) => value?.toLowerCase().trim())
email: string;
@IsString()
@MinLength(2)
@MaxLength(100)
name: string;
@IsString()
@MinLength(8)
@MaxLength(128)
password: string;
@IsOptional()
@IsEnum(['user', 'admin'])
role: 'user' | 'admin' = 'user';
}
src/users/users.module.ts — module yang mengikat semuanya:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
Diagram Dependency Injection #
flowchart LR
AppModule[AppModule] -->|imports| UsersModule
UsersModule -->|controllers| UsersController
UsersModule -->|providers| UsersService
UsersModule -->|imports| TypeOrmModule
TypeOrmModule -->|provides Repository| Repo[(User Repository)]
UsersController -->|DI: constructor| UsersService
UsersService -->|DI: @InjectRepository| Repo
Perhatikan constructor diUsersControllerdanUsersService— tidak adanew UsersService(). NestJS yang meng-inject instance-nya. Ini bukan magic: NestJS membaca type dari parameter TypeScript (UsersService,Repository<User>) lewatreflect-metadata, lalu resolve dari container. Hasilnya: kode lebih mudah di-test (kamu bisa ganti service dengan mock di unit test) dan loose coupling.
Main Entry Point #
src/main.ts:
import { NestFactory } from '@nestjs/core';
import { ValidationPipe, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log'], // default logger
});
const config = app.get(ConfigService);
// Global validation pipe untuk semua DTO
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // strip field yang tidak ada di DTO
forbidNonWhitelisted: true, // error 400 jika ada field ekstra
transform: true, // transform payload ke instance class
transformOptions: {
enableImplicitConversion: true,
},
}),
);
// Prefix global opsional: app.setGlobalPrefix('api/v1');
// Listen di 0.0.0.0 agar container bisa diakses dari host
const port = config.get<number>('PORT') || 3000;
await app.listen(port, '0.0.0.0');
Logger.log(`Application running on port ${port}`, 'Bootstrap');
}
bootstrap().catch((err) => {
// eslint-disable-next-line no-console
console.error('Failed to start application', err);
process.exit(1);
});
await app.listen(port, '0.0.0.0')—0.0.0.0wajib untuk Docker. Kalau kamu listen di127.0.0.1, container hanya menerima koneksi dari dalam container, bukan dari host.
ValidationPipedenganwhitelist: trueadalah proteksi penting — tanpa whitelist, body yang dikirim client bisa berisi field tambahan yang akan ikut tersimpan ke database.forbidNonWhitelisted: truemenolak request dengan field ekstra, mengembalikan error 400.transform: truemengkonversi payload JSON ke instance class DTO, sehingga decoratorclass-validatorbisa berjalan.
Health Check Endpoint #
Health check wajib untuk containerized app. Docker, Kubernetes, dan load balancer akan ping endpoint ini untuk tahu apakah app siap menerima request.
src/health/health.controller.ts:
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private readonly health: HealthCheckService,
private readonly db: TypeOrmHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('database'),
]);
}
}
Tambahkan @nestjs/terminus ke dependencies dan daftarkan HealthModule di AppModule.
Testing dengan @nestjs/testing #
NestJS punya helper testing bawaan yang meng-handle dependency injection container untuk test.
src/users/users.service.spec.ts:
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ConflictException, NotFoundException } from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
describe('UsersService', () => {
let service: UsersService;
let mockRepository: any;
beforeEach(async () => {
mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
findAndCount: jest.fn(),
create: jest.fn(),
save: jest.fn(),
remove: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepository },
],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should throw NotFoundException for missing user', async () => {
mockRepository.findOne.mockResolvedValue(null);
await expect(service.findOne(999)).rejects.toThrow(NotFoundException);
});
it('should throw ConflictException for duplicate email', async () => {
mockRepository.findOne.mockResolvedValue({ id: 1, email: '[email protected]' });
await expect(
service.create({ email: '[email protected]', name: 'Test', password: '12345678' })
).rejects.toThrow(ConflictException);
});
});
@nestjs/testing membuat module testing yang mirip dengan module production. Kamu bisa ganti provider dengan mock lewat useValue, useClass, atau useFactory.
Build dan Run #
# Build image
docker compose build
# Jalankan semua service
docker compose up -d
# Lihat log
docker compose logs -f api
# Test endpoint
curl http://localhost:3000/api/v1/users
# Cek health
curl http://localhost:3000/health
# Stop
docker compose down
# Reset database (hapus volume)
docker compose down -v
Akses:
| Service | URL |
|---|---|
| API | http://localhost:3000 |
| Health | http://localhost:3000/health |
| Postgres | localhost:5432 (user app, password dev) |
| Redis | localhost:6379 |
Untuk debug NestJS dari dalam container, tambahkan port debug ke service
apididocker-compose.yml:ports: - "3000:3000" - "9229:9229" # Node.js debuggerLalu di VS Code, buat launch configuration dengan
port: 9229danaddress: localhost. Set breakpoint disrc/users/users.service.tslalu trigger request — VS Code akan break.
Best Practice #
1. Selalu Pisahkan Module #
Setiap domain (users, orders, products) harus punya module sendiri. Module saling independen dan berkomunikasi via service yang di-export. Menghindari god module yang berisi semua controller dan service.
2. Pakai DTO dan Class-Validator #
DTO memvalidasi input dan mengubahnya ke instance class. Tanpa DTO, req.body adalah plain object tanpa validasi, dan decorator tidak bekerja.
3. Exception Filter untuk Response Konsisten #
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception instanceof HttpException
? exception.message
: 'Internal server error';
response.status(status).json({
statusCode: status,
path: request.url,
method: request.method,
message,
timestamp: new Date().toISOString(),
});
}
}
Daftarkan di main.ts: app.useGlobalFilters(new AllExceptionsFilter()).
4. Pakai ConfigService, Bukan process.env #
// ANTI-PATTERN
const port = process.env.PORT || 3000;
// BENAR
const config = app.get(ConfigService);
const port = config.get<number>('PORT') || 3000;
ConfigService validasi tipe dan meng-inject config module. Lebih aman, dan test-friendly.
5. Non-Root User di Production #
Stage runner di Dockerfile sudah menjalankan container sebagai user app. Mencegah privilege escalation.
6. Pakai Pino untuk Logging #
Default logger NestJS cukup untuk development, tapi nestjs-pino lebih cepat dan mendukung structured logging.
7. Healthcheck Endpoint #
Endpoint /health harus bisa di-ping Docker. Pakai @nestjs/terminus untuk cek dependency (Postgres, Redis).
Kapan NestJS, Kapan Express? #
| Kriteria | Pilih NestJS | Pilih Express |
|---|---|---|
| Struktur tim besar | ✓ | – |
| Butuh DI container | ✓ | – |
| Codebase > 50k LOC | ✓ | – |
| Microservice dengan pattern konsisten | ✓ | – |
| Script kecil / prototype | – | ✓ |
| API gateway ringan | – | ✓ |
| Aplikasi event-driven | ✓ (RxJS) | – |
| Butuh fleksibilitas struktur | – | ✓ |
NestJS bukan “penerus” Express — keduanya adalah tool berbeda untuk skala berbeda. Express ideal untuk script 50 baris; NestJS ideal untuk monorepo 200+ file dengan 10 developer. Memilih framework yang “lebih besar” untuk masalah kecil adalah over-engineering.
Troubleshooting #
Watch Mode Tidak Restart #
Pastikan nest start --watch di script start:dev, dan bind mount ./src:/app/src di Compose. Cek log: docker compose logs api — harus ada pesan [Nest] Starting Nest application setiap kali ada perubahan.
TypeORM Cannot Connect #
Error: connect ECONNREFUSED 127.0.0.1:5432
Hostname salah. Hostname di dalam container adalah nama service Compose, bukan localhost. Set DATABASE_HOST: db (bukan localhost).
reflect-metadata Tidak Ditemukan #
Pastikan import 'reflect-metadata' ada di paling atas main.ts. Tanpa import ini, decorator tidak punya metadata dan DI gagal resolve.
Validation Pipe Tidak Menolak Field Asing #
Tambahkan whitelist: true dan forbidNonWhitelisted: true ke ValidationPipe. Tanpa whitelist, field di body yang tidak ada di DTO akan lolos.
Ringkasan #
- NestJS ideal untuk tim yang butuh struktur, DI, dan module system. Hot reload via
nest start --watchmembuat iterasi cepat tanpa keluar dari container.- Dockerfile multi-stage: stage
depsuntuk development (semua dependencies), stagebuilderuntuk compile, stagerunnerramping untuk production dengan non-root user.- Anonymous volume
/app/node_modulesdan/app/distwajib — melindungi dependency dan build output dari override host.- Module system mengorganisir codebase jadi unit yang kohesi tinggi. Setiap domain punya module sendiri.
- Decorator (
@Controller,@Get,@Injectable) adalah metadata untuk runtime. ButuhexperimentalDecoratorsdanemitDecoratorMetadataditsconfig.json.- Dependency Injection lewat constructor — NestJS resolve dan inject service. Tidak ada
new Service()di kode aplikasi.- DTO + class-validator untuk validasi input.
ValidationPipeglobal denganwhitelist: truedanforbidNonWhitelisted: truemencegah field liar masuk.- Entity TypeORM merepresentasikan tabel database. Repository di-inject via
@InjectRepository(Entity).- Exception filter global untuk response error konsisten di seluruh endpoint.
- Health endpoint dengan
@nestjs/terminusping database. Wajib untuk container orchestration.- Testing dengan
@nestjs/testing+ mock repository. Tidak perlu start Postgres untuk unit test.- Kapan pilih NestJS: codebase besar, tim besar, butuh DI. Kapan pilih Express: script kecil, prototype, API gateway ringan.
- Troubleshooting: watch mode (cek volume), DB connection (hostname service, bukan localhost), validation (whitelist), decorator (reflect-metadata + tsconfig).
- Production: non-root user, healthcheck, structured logging, separate Dockerfile stage, environment variable via ConfigService.