A training camp forgives everything. The rampart is low, because nobody storms it anyway; provisions arrive daily; and if something collapses, it goes back up after lunch. A fort on the frontier forgives nothing - not because it was built differently, but because nobody is standing beside it to put things right.
Deployment is that moment of moving out. The application you have been starting with
yarn start:dev must now run on its own, for many days, for people you do not know. This lesson is about the four things to do before that happens.In development mode NestJS translates TypeScript as you go, on every file save. In production we translate once, in advance:
1{
2 "scripts": {
3 "build": "nest build",
4 "start:dev": "nest start --watch",
5 "start:prod": "node dist/main"
6 }
7}The
command compiles TypeScript to JavaScript in the nest build
directory - and that is all. It does not generate Swagger documentation (that is added separately, with decorators and dist/
SwaggerModule), it does not install dependencies from package.json (that is what yarn install is for), it does not start watch mode (that is start:dev with the --watch flag).After compiling you run
- plain Node.js on a plain node dist/main
.js file. On a production server TypeScript is no longer needed, because there is nothing left to translate.NODE_ENV is an environment variable read by nearly every library in the Node.js ecosystem. Setting NODE_ENV=production enables production optimisations and disables verbose logging.That is all it does. It does not run automated tests -
yarn test does. It does not enable hot-reload mode; quite the opposite, hot-reload belongs to development. It does not generate API documentation. Changing one string adds no features to an application - it merely shifts what is already there from "help the developer" mode into "serve traffic" mode.The consequences can be surprisingly large: libraries skip expensive checks, template caching is switched on, and stack traces stop appearing in error responses - because a user has no use for them and an attacker has a great deal.
NODE_ENV is only one of them. The database address, the secret for signing tokens, the payment gateway key - all of these are environment variables: values handed to the application from outside instead of written into the code.1NODE_ENV=production
2PORT=3000
3DATABASE_URL=postgres://legion:password@db.limes.internal:5432/tributes
4JWT_SECRET=change-me-before-marchingThe reason is practical: the same built
dist/ must run on your machine, on the test environment and in production. All that differs between them is the set of variables. If the database address sat in the code, every environment would need its own compilation - and then what you tested would not be what you run.That is why configuring the variables is the first step of preparation, before any compilation.
The
main.ts file starts the application. In its development form it usually runs to two lines; in production four things are added to it, in this order:1async function bootstrap() {
2 const app = await NestFactory.create(AppModule);
3
4 app.use(helmet());
5 app.use(compression());
6 app.enableCors({ origin: 'https://legion.imperium.rome' });
7
8 app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
9
10 const port = process.env.PORT || 3000;
11 await app.listen(port);
12}The order is no accident:
must come first, because only then does the application object exist; const app = await NestFactory.create(AppModule)
and the other middleware are set before any traffic is served; app.use(helmet())
registers validation; app.useGlobalPipes(new ValidationPipe())
opens the port, and from that moment the application accepts requests. Anything you write after await app.listen(port)
listen runs with the server already up.Each of those four does something different:
helmet sets HTTP security headers - Content-Security-Policy (CSP), X-Frame-Options, Strict-Transport-Security and a dozen more. It does not encrypt the database, does not compress static files and does not manage user sessions; it is a helmet for responses, not for data.compression packs responses with gzip before they go out over the network. A JSON of several hundred kilobytes can come down to a few dozen.enableCors settles which sites a browser may call your API from. In production you name a specific address, not a wildcard.ValidationPipe is the input guard you already know, switched on globally here.The whole road to production falls into an order that cannot be shuffled:
helmet, compression and CORS - because these are changes to the code, and the code is about to be frozen.nest build - the dist/ directory appears, unchanging from that moment.node dist/main in production - what you built starts up, with whatever variables it finds.Swapping steps two and three is the commonest mistake on this list: a built
dist/ knows nothing of changes you wrote after compiling. The application will come up, report no error at all, and run without security headers.The fort sets out for the frontier, @name:
nest build compiles TypeScript to JavaScript in the dist/ directory - it does not generate Swagger, does not install dependencies, does not start watch mode,node dist/main; TypeScript is no longer needed there,NODE_ENV=production enables production optimisations and disables verbose logging - it does not run tests, does not enable hot-reload, does not generate documentation,main.ts: const app = await NestFactory.create(AppModule) → app.use(helmet()) → app.useGlobalPipes(new ValidationPipe()) → await app.listen(port),helmet sets HTTP security headers (CSP, X-Frame-Options and others) - it does not encrypt the database, does not compress static files, does not manage sessions,compression packs responses with gzip, enableCors names the permitted request origins,helmet, compression, CORS → nest build → node dist/main.In the next lesson we shall close the fort inside a container, so that its interior looks the same on every machine. For now remember: production is not a harder version of development. It is the same application with nobody there to put things right.