https / fetch
node:https for clients + servers. TLS certs, custom CA, mTLS, and the patterns for production HTTPS.
Node — node:https
EXAMPLE
import https from 'node:https';
import fs from 'node:fs/promises';
// ===== HTTPS client =====
const data = await new Promise((resolve, reject) => {
https.get('https://api.github.com/repos/nodejs/node', { headers: { 'user-agent': 'demo' } }, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => resolve(JSON.parse(body)));
res.on('error', reject);
});
});
// Easier: fetch() (built-in since Node 18):
const r = await fetch('https://api.github.com/repos/nodejs/node', { headers: { 'user-agent': 'demo' } });
const json = await r.json();
// ===== HTTPS server =====
const server = https.createServer({
key: await fs.readFile('./key.pem'),
cert: await fs.readFile('./cert.pem'),
}, (req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' });
res.end('hello tls\n');
});
server.listen(8443, () => console.log('https://localhost:8443'));
// ===== Self-signed cert for dev =====
// openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'
// In dev clients, allow self-signed:
const r2 = await fetch('https://localhost:8443', { agent: new https.Agent({ rejectUnauthorized: false }) });
// NEVER do this in production. Use Let's Encrypt / mkcert for local TLS.
// ===== Custom CA bundle (corporate proxies) =====
const ca = await fs.readFile('/etc/ssl/corp-ca.pem');
const r3 = await fetch('https://internal.corp', { agent: new https.Agent({ ca }) });
// Or set globally:
// process.env.NODE_EXTRA_CA_CERTS = '/etc/ssl/corp-ca.pem';
// ===== mTLS (client certificate auth) =====
const r4 = await fetch('https://api.internal', {
agent: new https.Agent({
cert: await fs.readFile('./client.crt'),
key: await fs.readFile('./client.key'),
ca: await fs.readFile('./ca.crt'),
}),
});
// ===== HTTP/2 =====
import http2 from 'node:http2';
const h2 = http2.createSecureServer({
key: await fs.readFile('./key.pem'),
cert: await fs.readFile('./cert.pem'),
});
h2.on('stream', (stream, headers) => {
stream.respond({ ':status': 200, 'content-type': 'text/plain' });
stream.end('h2 hello');
});
h2.listen(8443);
// ===== In production =====
// Usually you terminate TLS at a reverse proxy (nginx, Caddy, ELB, ALB).
// Node serves plain HTTP behind it. Simpler ops, fewer cert renewals in app code.
// ===== Patterns to internalise =====
// - fetch() for clients in modern Node
// - Terminate TLS at the edge in prod; Node speaks HTTP
// - Custom CA via NODE_EXTRA_CA_CERTS, not rejectUnauthorized: false
// - HTTP/2 only when you want multiplex + push (most cases stick to /1.1)
// ===== Pitfalls =====
// - rejectUnauthorized: false in production -> MITM accepted
// - Forgetting to renew certs (Let's Encrypt + auto-renew)
// - Self-signed certs in CI tests without --insecure-ca handling
// - HTTP/2 streams not properly closed -> connection leaks
Why it matters
fetch() handles most HTTPS clients today; node:https is still there for low-level control + mTLS + custom CAs. In production terminate TLS at the edge proxy; in dev use mkcert for trusted local certs. NODE_EXTRA_CA_CERTS over rejectUnauthorized: false.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Node 18+ has fetch built in.
const r = await fetch('https://httpbin.org/json');
console.log(await r.json());
Try it Yourself »
Discussion
Loading…