NestJS #

NestJS is a Node.js backend framework combining Object-Oriented Programming, Functional Programming, and Reactive Programming in one structured architecture. Born from the “Angular for backend” philosophy, NestJS brings the module system, decorators, and dependency injection to the Node.js world — three pillars familiar to Spring, Laravel, or .NET developers, yet still lightweight and idiomatic in TypeScript.

For local development, NestJS has unique challenges: its codebase is large, its dependencies are numerous (TypeScript, ts-node, the Nest CLI, reflector metadata, RxJS), and hot reload must work inside a container without losing TypeScript state. Docker is the most solid answer — you standardize the toolchain, dependencies, and supporting services in one stack that can be re-run with a single command.

This article covers a Docker Compose setup for NestJS local development in depth: multi-stage Dockerfiles, hot reload via nest start --watch, the module structure, controllers, services, DTOs, validation pipes, dependency injection, TypeORM with Postgres, and production-grade best practices.

Prerequisites #

Make sure the host has installed:

  • Docker and Docker Compose (latest versions, v2.20+)
  • Node.js 20+ — for host tooling (optional, only if you want to run the Nest CLI without Docker)
  • The Nest CLInpm i -g @nestjs/cli (optional, for manual scaffolding)

The assumed NestJS project structure:

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 has a powerful CLI: nest new, nest g module users, nest g controller users, nest g service users. The CLI generates files with correct boilerplate and updates modules automatically. Use the CLI for new projects, manual edits for existing projects.

Multi-Stage Dockerfile #

NestJS is TypeScript — source code needs compiling to JavaScript before running. For development, we run directly via ts-node-dev or nest start --watch (more recommended). For production, we compile to dist/. The Dockerfile below separates the two.

# syntax=docker/dockerfile:1.6
FROM node:20-alpine AS deps

WORKDIR /app

# Copy the manifest first for layer caching
COPY package.json package-lock.json ./

# Install all dependencies (including devDependencies)
RUN npm install --no-audit --no-fund

# Copy TypeScript and Nest CLI configuration
COPY tsconfig.json tsconfig.build.json nest-cli.json ./

# Copy 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

# Copy only production dependencies
COPY package.json package-lock.json ./
RUN npm install --omit=dev --no-audit --no-fund

# Copy the build output
COPY --from=builder --chown=app:app /app/dist ./dist

USER app

EXPOSE 3000

CMD ["node", "dist/main.js"]

Stage Explanations #

The deps stage — the development foundation. Installs all dependencies (including TypeScript, the Nest CLI, ts-node) and copies source code. This is the stage used for daily docker compose up.

The builder stage — compiles TypeScript to dist/. Useful for testing the production image locally or in CI.

The runner stage — a slim image. Only includes dist/ and production dependencies. A non-root user for security.

Why multi-stage? Local dev uses the deps stage (has hot reload); production builds and tests can use the builder or runner stages without changing the Dockerfile.

Don’t copy node_modules from the deps stage to the runner stage. The deps stage contains devDependencies that must not exist in production. Always run npm install --omit=dev in the runner stage so only the dependencies in package.json get installed.

docker-compose.yml for 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 for build output
    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:

Service Explanations #

api — the NestJS service. target: deps selects the development stage. command: npm run start:dev runs the start:dev script in package.json, which we set to nest start --watch. Watch mode watches file changes in src/ and restarts the server automatically.

Two anonymous volumes for node_modules and dist — both mandatory:

  • /app/node_modules prevents the host from overwriting dependencies
  • /app/dist prevents locally built dist/ output from riding along in the container (which could conflict with watch mode)

db and cache — supporting services with healthchecks. The NestJS app won’t start until Postgres and Redis are truly ready.

Architecture Diagram #

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 with nest start –watch #

NestJS has built-in watch mode. Since Nest CLI version 9, watch mode uses tsc watch, which monitors TypeScript changes, recompiles, and restarts the 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 and experimentalDecorators: true are mandatory for NestJS. Decorators don’t work in NestJS without these two options. When creating a project with nest new, the CLI includes both by default.

.dockerignore:

node_modules
dist
coverage
.git
.gitignore
.env
.env.local
Dockerfile
docker-compose.yml
.dockerignore
README.md
*.md
test

NestJS Architecture — Module, Controller, Service #

NestJS has three core concepts that set it apart from other Node.js frameworks: Module, Controller, and Service — plus Dependency Injection, which binds them together.

Module #

A module is an organizational unit. Each module groups controllers, services, and other providers with high cohesion. The root module (AppModule) imports all other modules.

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: [
    // Global ConfigModule, reads from .env
    ConfigModule.forRoot({
      isGlobal: true,
      cache: true,
    }),

    // TypeORM async config — reads from 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 #

A controller is the layer that receives HTTP requests, validates input, and returns responses. The @Controller, @Get, @Post decorators are declared above classes and methods.

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)            // all endpoints need 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 #

Services contain business logic. NestJS injects services into controllers via the constructor — that’s its Dependency Injection.

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 with ID ${id} not found`);
    }
    return user;
  }

  async create(createUserDto: CreateUserDto): Promise<User> {
    const existing = await this.userRepository.findOne({
      where: { email: createUserDto.email },
    });
    if (existing) {
      throw new ConflictException('Email already registered');
    }

    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);            // throws 404 if missing
    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);
  }
}

Entities and DTOs #

src/users/entities/user.entity.ts — a TypeORM entity:

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 — a DTO with 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 — the module binding it all together:

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 {}

Dependency Injection Diagram #

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
Notice the constructors in UsersController and UsersService — there’s no new UsersService(). NestJS injects the instance. This isn’t magic: NestJS reads the parameter types from TypeScript (UsersService, Repository<User>) via reflect-metadata, then resolves them from the container. The result: easier-to-test code (you can swap the service with a mock in unit tests) and loose coupling.

The 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 for all DTOs
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,            // strip fields not in the DTO
      forbidNonWhitelisted: true, // error 400 on extra fields
      transform: true,            // transform the payload into class instances
      transformOptions: {
        enableImplicitConversion: true,
      },
    }),
  );

  // Optional global prefix: app.setGlobalPrefix('api/v1');

  // Listen on 0.0.0.0 so the container is reachable from the 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 is mandatory for Docker. If you listen on 127.0.0.1, the container only accepts connections from inside the container, not from the host.

ValidationPipe with whitelist: true is important protection — without whitelist, client-sent bodies can contain extra fields that get persisted to the database. forbidNonWhitelisted: true rejects requests with extra fields, returning an error 400. transform: true converts JSON payloads into DTO class instances, so class-validator decorators can run.

Health Check Endpoints #

Health checks are mandatory for containerized apps. Docker, Kubernetes, and load balancers ping this endpoint to know whether the app is ready for requests.

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'),
    ]);
  }
}

Add @nestjs/terminus to dependencies and register HealthModule in AppModule.


Testing with @nestjs/testing #

NestJS has built-in testing helpers that handle the dependency injection container for tests.

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 creates a testing module similar to the production module. You can swap providers with mocks via useValue, useClass, or useFactory.


Build and Run #

# Build the image
docker compose build

# Run all services
docker compose up -d

# View logs
docker compose logs -f api

# Test an endpoint
curl http://localhost:3000/api/v1/users

# Check health
curl http://localhost:3000/health

# Stop
docker compose down

# Reset the database (remove volumes)
docker compose down -v

Access:

ServiceURL
APIhttp://localhost:3000
Healthhttp://localhost:3000/health
Postgreslocalhost:5432 (user app, password dev)
Redislocalhost:6379

To debug NestJS from inside the container, add a debug port to the api service in docker-compose.yml:

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

Then in VS Code, create a launch configuration with port: 9229 and address: localhost. Set a breakpoint in src/users/users.service.ts, then trigger a request — VS Code will break.


Best Practices #

1. Always Separate Modules #

Each domain (users, orders, products) must have its own module. Modules are independent and communicate via exported services. Avoid a “god module” containing all controllers and services.

2. Use DTOs and Class-Validator #

DTOs validate input and convert it into class instances. Without DTOs, req.body is a plain object without validation, and decorators don’t work.

3. Exception Filters for Consistent Responses #

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(),
    });
  }
}

Register in main.ts: app.useGlobalFilters(new AllExceptionsFilter()).

4. Use ConfigService, Not process.env #

// ANTI-PATTERN
const port = process.env.PORT || 3000;

// CORRECT
const config = app.get(ConfigService);
const port = config.get<number>('PORT') || 3000;

ConfigService validates types and injects the config module. Safer and test-friendly.

5. Non-Root Users in Production #

The runner stage in the Dockerfile already runs the container as the app user. Prevents privilege escalation.

6. Use Pino for Logging #

The default NestJS logger is fine for development, but nestjs-pino is faster and supports structured logging.

7. Healthcheck Endpoints #

The /health endpoint must be pingable by Docker. Use @nestjs/terminus to check dependencies (Postgres, Redis).


When NestJS, When Express? #

CriterionChoose NestJSChoose Express
Large team structure
Need a DI container
Codebase > 50k LOC
Microservices with consistent patterns
Small scripts / prototypes
Lightweight API gateways
Event-driven apps✓ (RxJS)
Need structural flexibility
NestJS isn’t Express’s “successor” — they’re different tools for different scales. Express is ideal for a 50-line script; NestJS is ideal for a 200+ file monorepo with 10 developers. Choosing a “bigger” framework for a small problem is over-engineering.

Troubleshooting #

Watch Mode Not Restarting #

Make sure nest start --watch is in the start:dev script, and the ./src:/app/src bind mount is in Compose. Check the logs: docker compose logs api — there should be a [Nest] Starting Nest application message on every change.

TypeORM Cannot Connect #

Error: connect ECONNREFUSED 127.0.0.1:5432

Wrong hostname. Inside the container, the hostname is the Compose service name, not localhost. Set DATABASE_HOST: db (not localhost).

reflect-metadata Not Found #

Make sure import 'reflect-metadata' is at the very top of main.ts. Without this import, decorators have no metadata and DI fails to resolve.

Validation Pipe Not Rejecting Foreign Fields #

Add whitelist: true and forbidNonWhitelisted: true to the ValidationPipe. Without whitelist, body fields not in the DTO slip through.


Summary #

  • NestJS is ideal for teams needing structure, DI, and a module system. Hot reload via nest start --watch keeps iteration fast without leaving the container.
  • Multi-stage Dockerfiles: a deps stage for development (all dependencies), a builder stage for compiling, a slim runner stage for production with a non-root user.
  • The anonymous volumes /app/node_modules and /app/dist are mandatory — they protect dependencies and build output from host overrides.
  • The module system organizes the codebase into high-cohesion units. Each domain has its own module.
  • Decorators (@Controller, @Get, @Injectable) are runtime metadata. Need experimentalDecorators and emitDecoratorMetadata in tsconfig.json.
  • Dependency Injection via constructors — NestJS resolves and injects services. No new Service() in application code.
  • DTOs + class-validator for input validation. A global ValidationPipe with whitelist: true and forbidNonWhitelisted: true keeps stray fields out.
  • TypeORM entities represent database tables. Repositories are injected via @InjectRepository(Entity).
  • Global exception filters for consistent error responses across all endpoints.
  • Health endpoints with @nestjs/terminus ping the database. Mandatory for container orchestration.
  • Testing with @nestjs/testing + mock repositories. No need to start Postgres for unit tests.
  • When to choose NestJS: large codebases, large teams, need DI. When to choose Express: small scripts, prototypes, lightweight API gateways.
  • Troubleshooting: watch mode (check volumes), DB connections (service hostname, not localhost), validation (whitelist), decorators (reflect-metadata + tsconfig).
  • Production: non-root users, healthchecks, structured logging, separate Dockerfile stages, environment variables via ConfigService.

← Previous: Express.js   Next: Fastify →

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