We use cookies to enhance your experience on the site
CodeWorlds

Deployment - marching the fort into the field

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, @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,
  • 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.

Go to CodeWorlds