NestJS course Β· Module 9: Deployment and Infrastructure

Deployment - marching the fort into the field

6 min read
In this lesson6

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.

nest build - compilation

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 nest build command compiles TypeScript to JavaScript in the dist/ directory - and that is all. It does not generate Swagger documentation (that is added separately, with decorators and 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 node dist/main - plain Node.js on a plain .js file. On a production server TypeScript is no longer needed, because there is nothing left to translate.

NODE_ENV - one string, many consequences

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.

Environment variables - what separates a fort from a camp

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-marching

The 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.

A production main.ts

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: const app = await NestFactory.create(AppModule) must come first, because only then does the application object exist; app.use(helmet()) and the other middleware are set before any traffic is served; app.useGlobalPipes(new ValidationPipe()) registers validation; await app.listen(port) opens the port, and from that moment the application accepts requests. Anything you write after 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.

Four steps

The whole road to production falls into an order that cannot be shuffled:

  1. Configure environment variables - because they decide which database the application connects to.
  2. Add helmet, compression and CORS - because these are changes to the code, and the code is about to be frozen.
  3. Run nest build - the dist/ directory appears, unchanging from that moment.
  4. Run 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.

Summary

The fort sets out for the frontier:

  • nest build compiles TypeScript to JavaScript in the dist/ directory - it does not generate Swagger, does not install dependencies, does not start watch mode,
  • in production you run 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,
  • environment variables let you run the same built directory on every environment - which is why they are configured first,
  • the order in 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,
  • four steps of preparation: environment variables β†’ 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.

Code for this lesson: src/deployment-intro.ts
1// Deployment - Taking the Fort to Open Sea
2import { NestFactory } from '@nestjs/core';
3import { ValidationPipe, Logger } from '@nestjs/common';
4
5// 1. Production configuration for main.ts
6async function bootstrap() {
7  const logger = new Logger('Bootstrap');
8
9  // We create the application with limited logging
10  const app = await NestFactory.create(AppModule, {
11    logger: ['error', 'warn', 'log'],
12  });
13
14  // Security - Helmet
15  // app.use(helmet());
16
17  // Response compression - reduces size by 60-80%
18  // app.use(compression());
19
20  // CORS - access control
21  app.enableCors({
22    origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
23    credentials: true,
24  });
25
26  // Global validation
27  app.useGlobalPipes(new ValidationPipe({
28    whitelist: true,           // Strip unknown fields
29    forbidNonWhitelisted: true, // Error on unknown fields
30    transform: true,           // Automatic type transformation
31  }));
32
33  // API prefix
34  app.setGlobalPrefix('api/v1');
35
36  // Graceful Shutdown - close connections before shutting down
37  app.enableShutdownHooks();
38
39  const port = process.env.PORT || 3000;
40  await app.listen(port, '0.0.0.0');
41
42  logger.log(`Application running on port ${port}`);
43  logger.log(`Environment: ${process.env.NODE_ENV}`);
44}
45
46// 2. Differences between environments
47// Development:  hot-reload, debug, verbose logging
48// Staging:      production-like, test data
49// Production:   optimized, limited logs, security
50
51// 3. Scripts in package.json
52const scripts = {
53  'start:dev': 'nest start --watch',
54  'start:debug': 'nest start --debug --watch',
55  'build': 'nest build',
56  'start:prod': 'node dist/main',
57  'test': 'jest',
58  'test:cov': 'jest --coverage',
59  'test:e2e': 'jest --config ./test/jest-e2e.json',
60  'lint': 'eslint "{src,test}/**/*.ts" --fix',
61};
62
63// 4. Pre-deployment checklist
64const checklist = [
65  'Tests passed (unit + e2e)',
66  'Environment variables set',
67  'Database migrated',
68  'Secrets secure (not in code!)',
69  'CORS configured',
70  'Health check endpoint ready',
71  'Monitoring enabled',
72  'Backup strategy ready',
73];
74

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. The nest build command in NestJS:

  2. 2. Setting NODE_ENV=production in a NestJS application:

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Configure main.ts with helmet, compression, CORS, and ValidationPipe for production

  • Vertical ordering

    Arrange the steps for preparing a NestJS application for production:

  • Click in order

    Arrange the main.ts configuration elements in the correct order:

Useful articles