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 CLInpm 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 copy node_modules dari stage deps ke stage runner. Stage deps berisi devDependencies yang tidak boleh ada di production. Selalu jalankan npm install --omit=dev di stage runner agar dependency yang ter-install hanya yang ada di dependencies di package.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_modules mencegah host menimpa dependency
  • /app/dist mencegah dist/ 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: true dan experimentalDecorators: true wajib untuk NestJS. Decorator di NestJS tidak bekerja tanpa dua opsi ini. Saat membuat project dengan nest 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 di UsersController dan UsersService — tidak ada new UsersService(). NestJS yang meng-inject instance-nya. Ini bukan magic: NestJS membaca type dari parameter TypeScript (UsersService, Repository<User>) lewat reflect-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.0 wajib untuk Docker. Kalau kamu listen di 127.0.0.1, container hanya menerima koneksi dari dalam container, bukan dari host.

ValidationPipe dengan whitelist: true adalah proteksi penting — tanpa whitelist, body yang dikirim client bisa berisi field tambahan yang akan ikut tersimpan ke database. forbidNonWhitelisted: true menolak request dengan field ekstra, mengembalikan error 400. transform: true mengkonversi payload JSON ke instance class DTO, sehingga decorator class-validator bisa 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 api di docker-compose.yml:

ports:
  - "3000:3000"
  - "9229:9229"   # Node.js debugger

Lalu di VS Code, buat launch configuration dengan port: 9229 dan address: localhost. Set breakpoint di src/users/users.service.ts lalu 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 --watch membuat iterasi cepat tanpa keluar dari container.
  • Dockerfile multi-stage: stage deps untuk development (semua dependencies), stage builder untuk compile, stage runner ramping untuk production dengan non-root user.
  • Anonymous volume /app/node_modules dan /app/dist wajib — 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. Butuh experimentalDecorators dan emitDecoratorMetadata di tsconfig.json.
  • Dependency Injection lewat constructor — NestJS resolve dan inject service. Tidak ada new Service() di kode aplikasi.
  • DTO + class-validator untuk validasi input. ValidationPipe global dengan whitelist: true dan forbidNonWhitelisted: true mencegah 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/terminus ping 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.

← Sebelumnya: Express.js   Berikutnya: Fastify →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact