NestJS Production Build

A production build compiles your TypeScript code to JavaScript, removes development tools, sets security configurations, and prepares your application to handle real user traffic reliably. Running a NestJS app in production requires a few specific steps beyond what you do during development.

Development vs Production Mode

Development (npm run start:dev):
  - TypeScript compiled on the fly with ts-node
  - Hot reload on file changes
  - Verbose error messages
  - synchronize: true (auto-creates DB tables)
  - Performance not optimized

Production (node dist/main):
  - Pre-compiled JavaScript runs directly
  - No file watching, no hot reload
  - Clean error responses (no stack traces to clients)
  - Migrations instead of synchronize
  - Optimized for performance and stability

Building the Application

npm run build

This command runs the TypeScript compiler (tsc) using the production config (tsconfig.build.json). The compiled JavaScript output appears in the dist/ folder.

dist/
├── main.js
├── app.module.js
├── users/
│   ├── users.controller.js
│   ├── users.service.js
│   └── user.entity.js
└── ...

Starting in Production

node dist/main.js
# or
npm run start:prod   # runs node dist/main.js

Production main.ts Configuration

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import helmet from 'helmet';
import * as compression from 'compression';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: ['log', 'warn', 'error', 'fatal'],  // no debug/verbose in prod
  });

  // Security headers
  app.use(helmet());

  // GZIP compression for responses
  app.use(compression());

  // Global validation
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }));

  // CORS (configure allowed origins per environment)
  app.enableCors({
    origin: process.env.ALLOWED_ORIGINS?.split(',') || false,
    credentials: true,
  });

  const port = process.env.PORT || 3000;
  await app.listen(port);
  console.log(`Application running on port ${port}`);
}
bootstrap();

Database Migrations Instead of synchronize

The synchronize: true option in TypeORM rewrites database tables to match your entities. In production, this can silently delete columns and lose data. Use migrations instead:

// typeorm production config
TypeOrmModule.forRoot({
  ...
  synchronize: false,    // never true in production
  migrations: ['dist/migrations/*.js'],
  migrationsRun: true,   // run pending migrations on startup
})

// Generate a migration after changing an entity
npx typeorm migration:generate src/migrations/AddUserRole -d src/datasource.ts

// Run all pending migrations
npx typeorm migration:run -d src/datasource.ts

Process Manager: PM2

PM2 keeps your Node.js application running, restarts it automatically if it crashes, and manages multiple instances for load balancing:

npm install -g pm2

# Start the app with PM2
pm2 start dist/main.js --name "nestjs-app"

# Start multiple instances (cluster mode — uses all CPU cores)
pm2 start dist/main.js --name "nestjs-app" -i max

# Auto-start on server reboot
pm2 startup
pm2 save

# Monitor running processes
pm2 monit

# View logs
pm2 logs nestjs-app

Production Checklist

Environment:
  [ ] NODE_ENV=production
  [ ] All secrets in environment variables (not in code)
  [ ] JWT secrets are long random strings (32+ chars)
  [ ] Database password is strong and unique

Application:
  [ ] synchronize: false (use migrations)
  [ ] helmet() enabled (security headers)
  [ ] compression() enabled
  [ ] Rate limiting enabled
  [ ] CORS configured to allow only known origins
  [ ] Global ValidationPipe with whitelist: true

Infrastructure:
  [ ] HTTPS/TLS certificate configured (via reverse proxy)
  [ ] Process manager (PM2) or container orchestration (Kubernetes)
  [ ] Health check endpoint (/health)
  [ ] Log aggregation configured
  [ ] Alerts for error spikes

Health Check Endpoint

npm install @nestjs/terminus

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.db.pingCheck('database'),
    ]);
  }
}

// Response when healthy:
{
  "status": "ok",
  "info": { "database": { "status": "up" } }
}

A health check endpoint lets load balancers and monitoring tools verify your application is running and its dependencies (database, cache) are reachable. Routing live traffic to a misconfigured or database-disconnected server wastes requests and frustrates users — the health endpoint prevents this automatically.

Leave a Comment

Your email address will not be published. Required fields are marked *