Chapter 18 of 18 · Node.js for Beginners — Complete Free Tutorial

Securing, Testing and Deploying Node Apps

The to-do API reads, writes, persists and renders — now it must survive contact with the internet. The final chapter of the Node.js for Beginners series is the production checklist: secrets out of source code, automated tests with Node's built-in runner, a health endpoint, graceful shutdown, and the deployment steps that turn a laptop project into a running service.

Security, in priority order

ControlWhat it stopsWhere
Secrets in environment variablesCredential leaks from Git history.env gitignored, values set per environment
Input validation on every writeGarbage data, injection, crashesRoute handlers (chapter 15)
Security headersClickjacking, MIME sniffing, XSS vectorsnpm install helmet, one app.use
Rate limitingBrute force and scrapersMiddleware registered before your routes
HTTPSEavesdropping and tamperingTerminated at the platform edge
No stack traces in responsesInternal paths and code disclosedGeneric 500 body (chapter 14)

Configuration belongs to the environment, never to the repository:

# .env — gitignored; commit .env.example with dummy values instead
PORT=3000
NODE_ENV=production
CORS_ORIGIN=https://app.example.com
API_TOKEN=replace-me-per-environment
const required = ['API_TOKEN', 'CORS_ORIGIN'];
for (const key of required) {
  if (!process.env[key]) {
    console.error(`Missing required environment variable: ${key}`);
    process.exit(1);            // fail fast at boot, not at first request
  }
}

Expected output: with a variable missing, the process refuses to start and prints the exact name of the problem — far kinder than a mysterious 401 an hour into production. Fail fast at boot, not slowly at runtime.

Tests with the built-in runner

Node ships a test runner and an assertion library — no packages required. Start the app on an ephemeral port (0 lets the OS pick a free one) and exercise it with the global fetch:

// test/api.test.js
import { test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { app } from '../src/app.js';

let server;
let base;

before(async () => {
  server = app.listen(0);
  base = `http://127.0.0.1:${server.address().port}`;
});
after(() => server.close());

test('GET /api/todos returns a list', async () => {
  const res = await fetch(`${base}/api/todos`);
  assert.equal(res.status, 200);
  const body = await res.json();
  assert.ok(Array.isArray(body.todos));
});

test('POST rejects a missing title', async () => {
  const res = await fetch(`${base}/api/todos`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({}),
  });
  assert.equal(res.status, 400);
});
node --test

Expected output:

TAP version 13
ok 1 - GET /api/todos returns a list
ok 2 - POST rejects a missing title
1..2
# tests 2
# pass 2
# fail 0

Add "test": "node --test" to your scripts so CI runs the same command you do. This is also why chapter 12 insisted on separating the app from app.listen: tests import the app and bind a random port without fighting over 3000.

Health checks and graceful shutdown

app.get('/healthz', (req, res) => {
  res.json({ status: 'ok', uptime: Math.round(process.uptime()) });
});

const server = app.listen(process.env.PORT ?? 3000, () => {
  console.log('listening');
});

function shutdown(signal) {
  console.log(`${signal} received — draining connections`);
  server.close(async () => {
    console.log('all connections finished');
    process.exit(0);
  });
  setTimeout(() => process.exit(1), 10_000).unref();  // hard deadline
}

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

Expected output: curl localhost:3000/healthz answers {"status":"ok","uptime":42}; sending SIGTERM prints the drain message, lets in-flight requests finish, then exits 0 — while curl /api/todos run during shutdown still completes. Platforms probe healthz to route traffic and send SIGTERM to stop you; implementing both is table stakes.

Deploying

  1. Build on the lock file — npm ci --omit=dev installs production dependencies exactly, no dev tools in the image.
  2. Run behind a supervisor — the platform's process manager, or PM2 on a VPS, restarts crashes and keeps you in the boot order.
  3. Terminate TLS at the edge — the platform or a reverse proxy handles HTTPS; your app speaks plain HTTP on an internal port.
  4. Send logs to stdout/stderr — no log files to rotate; collectors grab the streams (chapter 3's split finally pays off).
  5. Ship the data story — a persistent volume for data/todos.json, or move to a managed database before launch.
  6. Watch the health endpoint — alert when healthz fails; deploy only when it passes afterwards.
npm ci --omit=dev
NODE_ENV=production node src/server.js
curl -fsS http://localhost:3000/healthz

Expected output: {"status":"ok","uptime":0} — the service is up, answering, and ready to receive traffic from the proxy. Hosts such as Render, Railway, Fly.io or a plain VPS with Docker all follow this same recipe; only the manifest changes.

Common mistakes

  • Secrets committed to Git — history keeps them forever; rotate the value and move it to environment variables.
  • Testing only by hand — the moment validation changes, nothing notices until a user does.
  • Ignoring shutdown signals — Ctrl+C mid-write truncates the store; drain first, exit second.
  • Dev dependencies in production — bloated images and attack surface; --omit=dev is not optional.
  • No health check or logs — you cannot fix what you cannot see; ship both from day one.

Practice checklist

  1. Move every secret into environment variables and commit a documented .env.example.
  2. Write two passing tests and one deliberately failing test; read the TAP output for the failure detail.
  3. Add /healthz and verify it with curl before and after a request burst.
  4. Trigger SIGTERM during a slow request and confirm the response still completes.
  5. Deploy the app anywhere — even a free tier — and confirm the health check, HTTPS and a restart cycle all behave.

Congratulations: you read, wrote, persisted, exposed and shipped a real Node.js service. Keep the momentum going in the JavaScript best practices chapter, or browse everything else on the tutorials index.

Quiz: Securing, Testing and Deploying Node Apps

5 questions · 70% to pass

Frequently Asked Questions

Node.js is a runtime that executes JavaScript outside the browser. It combines Google's V8 engine with APIs for files, networks, processes and streams, plus a package ecosystem (npm). You use it to build servers, command-line tools, APIs and anything that needs JavaScript running on a machine rather than in a page.

JavaScript is the language; Node.js is one environment that runs it. In the browser the language comes with a DOM, events and window APIs — in Node those are replaced by the file system, network and process modules. Syntax, functions, arrays, promises and async/await are identical, so your JavaScript knowledge transfers directly.

Browser code is sandboxed: it cannot read your files or open arbitrary ports. Node code has full machine access, which is why it is used for servers and tools but also why you never run untrusted code in it. There is no document or window in Node — the equivalents are global, process and modules.

npm is JavaScript's package manager and public registry. package.json describes your project: name, entry point, dependencies with version ranges, and scripts such as npm run dev. Installing a package copies it into node_modules and records the exact intent in package-lock.json so every machine installs identical versions.

Express is a minimal web framework for Node: a few thousand lines that give you routing, middleware, request parsing and static files on top of the raw HTTP server. It is the most common starting point for REST APIs and the backbone of this tutorial's project chapters.

Both work. CommonJS (require/module.exports) is the classic Node format; ES modules (import/export) are the standard and activated in a package.json with "type": "module". Modern projects favour ESM for browser compatibility, but you will read both in existing code, so this course teaches each.

Middleware are functions that run between the incoming request and the final response, in order: body parsing, logging, authentication, rate limits, error handling. Each receives the request, response and next function and can respond itself or pass control along — Express's pipeline model in one sentence.

A JSON file is perfect for learning CRUD: no setup, readable data, easy to reset. The moment you need concurrent writes, querying or multiple users, move to a real database — MySQL/PostgreSQL for relational data or MongoDB for documents. The storage chapter isolates persistence behind one module so swapping it later is straightforward.

A REST API exposes resources over HTTP using verbs on URLs: GET /api/todos lists, POST creates, PUT or PATCH updates, DELETE removes, and everything answers with JSON plus correct status codes (200, 201, 400, 404, 500). It is stateless — each request carries its own authentication — which makes it easy to cache, scale and consume from any frontend.

Node apps are long-running processes, not static files: deploy them to a Node-friendly host (a VPS, Render, Railway or Fly.io), keep secrets in environment variables, terminate HTTPS at a reverse proxy or platform edge, and send logs somewhere durable. The final chapter covers the full checklist including health checks and graceful shutdown.