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.
