Skip to content
Node.js nd http 4 min read

HTTPS & TLS Servers

Serving traffic over plain HTTP sends every byte — passwords, tokens, payloads — across the wire in clear text. HTTPS wraps the same HTTP protocol inside a TLS (Transport Layer Security) tunnel that encrypts the connection and proves the server’s identity with a certificate. Node.js ships first-class support for this through the built-in node:https and node:tls modules, whose APIs mirror their plaintext node:http and node:net counterparts almost exactly. Once you understand how to load a key/certificate pair and tune the TLS handshake, securing a server is a small, well-defined step.

Creating an HTTPS server

https.createServer() has the same signature as http.createServer() plus an options object that carries your TLS credentials. The two required fields are key (your private key) and cert (the certificate that binds your domain to that key). Both are read as buffers or strings from PEM-encoded files.

import { createServer } from "node:https";
import { readFileSync } from "node:fs";

const options = {
  key: readFileSync("./certs/key.pem"),
  cert: readFileSync("./certs/cert.pem"),
};

const server = createServer(options, (req, res) => {
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ secure: true, url: req.url }));
});

server.listen(8443, () => {
  console.log("HTTPS server running at https://localhost:8443/");
});

The request and response objects are identical to those in the HTTP module, so existing handlers and frameworks work unchanged. The CommonJS form is the same aside from the import:

const { createServer } = require("node:https");

Read certificate files synchronously at startup, not inside the request handler. Loading them once avoids a disk hit on every connection and surfaces a misconfigured path immediately as a crash rather than a slow leak.

Generating self-signed certificates for development

Browsers reject certificates that no trusted authority signed, but for local development a self-signed certificate is perfectly fine — you just have to accept the browser warning or add the cert to your trust store. Generate a key and matching certificate with OpenSSL:

mkdir -p certs
openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout certs/key.pem -out certs/cert.pem \
  -days 365 -subj "/CN=localhost"

Output:

Generating a RSA private key
...........+++++
writing new private key to 'certs/key.pem'
-----

That writes certs/key.pem and certs/cert.pem, valid for 365 days, bound to the localhost common name. For a friendlier local experience, the mkcert tool creates certificates signed by a locally trusted root so browsers show no warning:

mkcert -install
mkcert localhost 127.0.0.1

In production you never self-sign — you obtain a free, browser-trusted certificate from Let’s Encrypt (via certbot) or your cloud provider, and renew it automatically.

The tls module basics

node:https is a thin layer over node:tls, which itself sits on top of node:net. When you need raw encrypted sockets without HTTP semantics — for example a custom binary protocol — use tls.createServer() and tls.connect() directly. They behave like net.createServer()/net.connect() but negotiate a TLS handshake first.

import { createServer } from "node:tls";
import { readFileSync } from "node:fs";

const server = createServer(
  {
    key: readFileSync("./certs/key.pem"),
    cert: readFileSync("./certs/cert.pem"),
  },
  (socket) => {
    console.log("client authorized:", socket.authorized);
    socket.write("welcome over TLS\n");
    socket.pipe(socket); // echo back
  },
);

server.listen(8443, () => console.log("TLS socket server on 8443"));

The socket passed to the listener is a tls.TLSSocket, a duplex stream you read from and write to like any other. Its authorized flag and getPeerCertificate() method let you inspect client certificates when you enable mutual TLS (mTLS).

Configuring TLS versions and ciphers

The defaults in modern Node.js are already secure, but production servers often need to pin a minimum protocol version for compliance and disable legacy ciphers. The most important option is minVersion — set it to "TLSv1.2" (or "TLSv1.3" if all clients support it) to refuse the long-broken TLS 1.0/1.1.

import { createServer } from "node:https";
import { readFileSync } from "node:fs";

const server = createServer({
  key: readFileSync("./certs/key.pem"),
  cert: readFileSync("./certs/cert.pem"),
  minVersion: "TLSv1.2",
  // TLS 1.2 cipher preference; TLS 1.3 suites are fixed and always on.
  ciphers: [
    "TLS_AES_256_GCM_SHA384",
    "ECDHE-RSA-AES256-GCM-SHA384",
    "ECDHE-RSA-AES128-GCM-SHA256",
  ].join(":"),
  honorCipherOrder: true,
});

server.listen(8443);

Common security-relevant options:

OptionPurpose
minVersion / maxVersionBound the negotiated TLS protocol (e.g. "TLSv1.2").
ciphersColon-separated cipher list for TLS 1.2 and below.
honorCipherOrderLet the server, not the client, pick the cipher.
caTrusted CA bundle for verifying client certs (mTLS).
requestCert / rejectUnauthorizedEnable and enforce mutual TLS.
ALPNProtocolsAdvertise protocols like ["h2", "http/1.1"].

Disabling certificate validation (rejectUnauthorized: false) is a frequent and dangerous shortcut. It silences “self-signed certificate” errors by turning off the very check that makes TLS trustworthy — never ship it. Fix the trust chain instead.

Best practices

  • Pin minVersion: "TLSv1.2" (prefer 1.3) and rely on Node’s secure default cipher suite rather than hand-rolling a fragile list.
  • Load keys and certificates once at startup with synchronous reads; restrict the private key file to chmod 600.
  • Never commit private keys to version control — keep certs/*.pem in .gitignore and inject production keys via secrets management.
  • Automate certificate renewal (Let’s Encrypt / ACME) and watch the files so the server reloads new certs without downtime.
  • In real deployments, terminate TLS at a hardened reverse proxy or load balancer (nginx, Caddy, a cloud LB) and let Node focus on application logic.
  • Enable HSTS by sending Strict-Transport-Security so browsers refuse to fall back to plain HTTP.
  • Use tls.createSecureContext() to rotate certificates at runtime instead of restarting the process.
Last updated June 14, 2026
Was this helpful?