In 2025, running a production Node.js application over plain HTTP is professional negligence. Modern browsers mark HTTP sites as “Not Secure”, CDNs refuse to cache them, and attackers can eavesdrop or tamper with traffic in seconds.

This is the definitive guide to properly implementing HTTPS and modern TLS in Node.js; from certificates to production-grade configuration.

https tls security

Why HTTPS and TLS Are Non-Negotiable in 2025

Risk Without HTTPS With Proper TLS
Confidentiality All data (tokens, passwords) in plain text End-to-end encryption (AES-256-GCM / ChaCha20)
Integrity Attacker can modify responses Tamper-proof with AEAD ciphers
Authenticity Phishing clones possible Proven via trusted certificates
Browser Trust “Not secure” warning Green lock / neutral UI
SEO & Performance Google penalty, no HTTP/2-3 Ranking boost + faster TLS 1.3
Modern Web APIs Blocked (Camera, Payments, WebAuthn) Fully enabled
Legal Compliance GDPR, PCI-DSS, DPDP violations Compliant

Bottom line: HTTPS is not optional anymore; it’s the bare minimum for any internet-facing application. If your Node.js app is still on HTTP, fix it today. There is zero technical or financial excuse left.

The Difference Between HTTPS and SSL/TLS

Term What It Is Role Analogy
SSL/TLS Cryptographic protocol (the lock) Encryption, integrity, authentication Armored van
HTTPS HTTP running over SSL/TLS (HTTP over TLS) The full secure application protocol Money inside the armored van

One-liner:

HTTPS = HTTP + SSL/TLS

Note: There is no SSL on the public internet in 2025; everything is TLS (1.2 minimum, 1.3 preferred). People still say “SSL certificate” out of habit.

In Simple Words

SSL/TLS = the security layer (the lock) HTTPS = normal HTTP + that security layer turned on

Think of it like sending a letter:

Normal HTTP → sending a postcard (anyone can read it) SSL/TLS → putting the letter in a locked safe HTTPS → sending the postcard inside the locked safe (so only the recipient can open and read it)

SSL/TLS is the encryption protocol. HTTPS is regular HTTP wrapped inside that encryption protocol.

SSL/TLS Versions in 2025: What You Should Run

Protocol Status (2025) Notes
SSL v2/v3 Dead & prohibited Removed from all browsers
TLS 1.0/1.1 Deprecated & blocked PCI-DSS non-compliant
TLS 1.2 Minimum acceptable Still widely supported
TLS 1.3 Gold standard Faster, simpler, more secure

Enforce TLS 1.3 + TLS 1.2 fallback only.

Getting a Trusted Certificate in 2025

Option A: Let’s Encrypt (Free & Automated) ; Still the Default

1
2
3
4
5
sudo certbot certonly --standalone -d api.yourdomain.com

# Files:
# /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem   cert + chain
# /etc/letsencrypt/live/api.yourdomain.com/privkey.pem     private key

Renewal hook (add to /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh or PM2):

1
2
#!/bin/bash
pm2 reload your-app-name   # or nginx -s reload, etc.

Option B: ZeroSSL, Google Trust Services, Sectigo

Use when you need wildcard or longer-validity certificates.

Minimal Production-Ready HTTPS Server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// server.js
import https from 'node:https';
import fs from 'node:fs';

const options = {
  key: fs.readFileSync('/etc/letsencrypt/live/api.yourdomain.com/privkey.pem'),
  cert: fs.readFileSync('/etc/letsencrypt/live/api.yourdomain.com/fullchain.pem'),

  // 2025 best practice
  minVersion: 'TLSv1.2',
  honorCipherOrder: true,
  ciphers: [
    'TLS_AES_256_GCM_SHA384',
    'TLS_CHACHA20_POLY1305_SHA256',
    'TLS_AES_128_GCM_SHA256'
  ].join(':')
};

https.createServer(options, (req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello Secure World!\n');
}).listen(443, () => console.log('HTTPS on :443'));

Real-World: Securing Express.js (Production Grade)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// src/server.ts
import express, { Request, Response, NextFunction } from 'express';
import https from 'node:https';
import fs from 'node:fs';
import helmet from 'helmet';
import { hsts } from 'hsts';

const app = express();

// Security headers
app.use(helmet({
  contentSecurityPolicy: { directives: { defaultSrc: ["'self'"] } },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));

// HSTS – 2 years + preload
app.use(hsts({
  maxAge: 63072000,
  includeSubDomains: true,
  preload: true
}));

// Redirect HTTP → HTTPS (only in production & behind proxy)
app.use((req: Request, res: Response, next: NextFunction) => {
  if (process.env.NODE_ENV === 'production' && req.headers['x-forwarded-proto'] !== 'https') {
    return res.redirect(301, `https://${req.headers.host}${req.url}`);
  }
  next();
});

app.get('/', (_req, res) => {
  res.send('Encrypted with TLS 1.3');
});

// Production HTTPS server
if (process.env.NODE_ENV === 'production') {
  const httpsOptions = {
    key: fs.readFileSync(process.env.SSL_KEY_PATH!),
    cert: fs.readFileSync(process.env.SSL_CERT_PATH!),
    minVersion: 'TLSv1.2',
    ciphers: 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256',
    honorCipherOrder: true,
  };

  https.createServer(httpsOptions, app).listen(443, () => {
    console.log('HTTPS listening on :443');
  });
} else {
  app.listen(4000, () => console.log('HTTP (dev) on :4000'));
}

Development: Trusted Self-Signed Certificates

Never test HTTPS-only features on plain HTTP.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Generate trusted local CA + cert (one-time)
openssl req -x509 -nodes -new -sha256 -days 1024 -newkey rsa:4096 \
  -keyout rootCA.key -out rootCA.pem -subj "/C=US/CN=LocalDev-CA"

openssl req -new -nodes -newkey rsa:2048 \
  -keyout localhost.key -out localhost.csr \
  -subj "/C=US/CN=localhost"

openssl x509 -req -in localhost.csr -CA rootCA.pem -CAkey rootCA.key \
  -CAcreateserial -extfile <(echo "subjectAltName=DNS:localhost,DNS:*.localhost,IP:127.0.0.1") \
  -out localhost.pem -days 365 -sha256

Trust rootCA.pem in your OS → green lock on localhost.

Behind Reverse Proxies (Nginx, Traefik, Cloudflare, ALB)

1
2
3
4
5
6
app.set('trust proxy', true); // Critical!

app.use((req, _res, next) => {
  // req.protocol, req.secure, req.hostname now correct
  next();
});

Set cookies with Secure; SameSite=None when needed.

Advanced: HTTP/3 (QUIC) in 2025

Node.js 21+ has experimental support via undici (client) and http3 (server). Production advice: Terminate QUIC at Cloudflare/Nginx and forward HTTP/2 to Node.js.

Deployment Checklist (Before You Go Live)

  • TLS 1.2 minimum, TLS 1.3 preferred
  • Let’s Encrypt or trusted paid cert with auto-renewal
  • HSTS header with preload (2 years + includeSubDomains)
  • Secure + HttpOnly + SameSite cookies
  • Helmet + strict CSP
  • HTTP → HTTPS 301 redirect
  • Test with SSL Labs → aim for A+ → https://www.ssllabs.com/ssltest/

Final Thoughts

In 2025, “We’ll add HTTPS later” is no longer acceptable.

  • Free certificates (Let’s Encrypt) remove cost barriers
  • TLS 1.3 is faster than plaintext in many cases
  • Modern Node.js makes correct TLS configuration trivial
  • Browser, legal, and performance requirements all demand it

Secure your traffic today. Your users, your future incident response team, and your reputation will thank you.