Building for Production
A NestJS project is written in TypeScript, but Node cannot run TypeScript directly — production servers run plain JavaScript. Building for production means compiling your src/ tree into an optimized dist/ folder, stripping development-only tooling, and signalling to every dependency that it is now in a production environment. Getting this step right produces faster startup, smaller images, and predictable runtime behaviour.
Running nest build
The Nest CLI wraps the TypeScript compiler (and, optionally, SWC or webpack) behind a single command. From a project scaffolded with nest new, the build is already wired into your package.json scripts.
npm run build
# which runs:
nest build
nest build reads tsconfig.build.json (an extension of your base tsconfig.json that excludes specs and the test/ directory), type-checks the project, and emits compiled output to the directory named in compilerOptions.outDir — dist by default. Anything that fails type-checking aborts the build, so a green build is also a type-safety gate.
Output:
> nest build
✔ Compilation succeeded (1.84s)
Understanding the dist output
After a successful build, dist/ mirrors your src/ structure with .js files plus source maps and .d.ts declarations. The entry point is dist/main.js, which is exactly what you run in production.
dist/
├── app.module.js
├── app.module.js.map
├── app.controller.js
├── app.service.js
└── main.js ← production entry point
Start the compiled app with Node directly — never with ts-node or nest start in production, both of which carry compilation overhead.
node dist/main.js
# or, via the generated script:
npm run start:prod
The start:prod script in package.json resolves to node dist/main, the canonical production launcher.
Tip: Add
dist/to.gitignoreand rebuild in CI rather than committing compiled artifacts. Staledist/files are a common source of “works locally, breaks in prod” bugs.
Choosing a compiler: tsc vs SWC
By default nest build uses tsc, which type-checks and emits in one pass. For large codebases, the SWC compiler is dramatically faster because it compiles without type-checking (you keep type safety in CI or your editor).
nest build --builder swc
| Builder | Type-checks | Speed | Best for |
|---|---|---|---|
| tsc | Yes | Baseline | Default; build-time type safety |
| swc | No (separate) | ~10-20x faster | Large repos, fast CI |
| webpack | Yes (via ts-loader) | Slower build | Single-file bundles |
Bundling with webpack
By default the output is a tree of many small modules that Node resolves at startup. Enabling webpack bundling collapses your application into a single main.js, which speeds cold starts and is especially valuable for serverless functions and lean container images.
nest build --webpack
For control over the bundle, add a webpack.config.js and reference it. Externalizing node_modules keeps the bundle small while letting heavy native dependencies load normally.
// webpack.config.js
const nodeExternals = require('webpack-node-externals');
module.exports = (options) => ({
...options,
externals: [nodeExternals()],
optimization: {
minimize: true,
},
});
nest build --webpack --webpackPath webpack.config.js
Warning: Bundling can break code that relies on dynamic module discovery (for example, glob-based entity or migration loading in TypeORM). Reference such files explicitly or keep them external so they resolve at runtime.
Setting NODE_ENV
NODE_ENV=production is the single most impactful runtime flag. Express disables verbose error stacks and enables view caching, many libraries skip development warnings, and your own ConfigService can branch on it. Set it in the environment, not in code.
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { Logger, ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
// quieter logs in production, verbose locally
logger:
process.env.NODE_ENV === 'production'
? ['error', 'warn', 'log']
: ['debug', 'verbose', 'log', 'warn', 'error'],
});
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
const port = process.env.PORT ?? 3000;
await app.listen(port);
Logger.log(`Listening on ${port} (${process.env.NODE_ENV})`, 'Bootstrap');
}
bootstrap();
NODE_ENV=production node dist/main.js
Trimming dev dependencies
A production install should contain only what the runtime needs. The Nest CLI, TypeScript, ESLint, Jest, and @types/* packages all live under devDependencies and have no place in a deployed artifact. Installing without them shrinks node_modules substantially and reduces the attack surface.
# install runtime dependencies only
npm ci --omit=dev
npm ci installs exactly what package-lock.json pins — reproducible and faster than npm install. In a Docker image, combine a full install for the build stage with a pruned install for the runtime stage.
# build stage: everything, then compile
npm ci
npm run build
# runtime stage: production deps + compiled dist only
npm ci --omit=dev
Output:
added 142 packages in 6s # vs ~480 with dev deps
Best Practices
- Build with
nest buildin CI and shipdist/; never commit compiled output or runts-nodein production. - Launch with
node dist/main.js(npm run start:prod), notnest start. - Set
NODE_ENV=productionin the environment so frameworks and libraries enable their optimized paths. - Install runtime dependencies with
npm ci --omit=devto keepnode_modulessmall and reproducible. - Reach for
--builder swcto speed large builds, and run type-checking separately so safety is never lost. - Use
--webpackwith externalizednode_modulesfor serverless and slim container deployments. - Keep secrets and per-environment values in environment variables, never baked into the bundle.