Node.js has built-in HTTPS support through the https module. You load your certificate files and create an HTTPS server. This guide covers raw Node.js, Express, and the recommended production setup.
Basic HTTPS server
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('/etc/ssl/privkey.pem'),
cert: fs.readFileSync('/etc/ssl/fullchain.pem'),
};
https.createServer(options, (req, res) => {
res.writeHead(200);
res.end('Hello HTTPS');
}).listen(443);
Express with HTTPS
const https = require('https');
const fs = require('fs');
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello HTTPS');
});
const options = {
key: fs.readFileSync('/etc/ssl/privkey.pem'),
cert: fs.readFileSync('/etc/ssl/fullchain.pem'),
};
https.createServer(options, app).listen(443, () => {
console.log('HTTPS server running on port 443');
});
Redirect HTTP to HTTPS
Run both an HTTP and HTTPS server:
const http = require('http');
const https = require('https');
const fs = require('fs');
const express = require('express');
const app = express();
// ... your routes
const options = {
key: fs.readFileSync('/etc/ssl/privkey.pem'),
cert: fs.readFileSync('/etc/ssl/fullchain.pem'),
};
// HTTPS server
https.createServer(options, app).listen(443);
// HTTP redirect
http.createServer((req, res) => {
res.writeHead(301, { Location: `https://${req.headers.host}${req.url}` });
res.end();
}).listen(80);
Production recommendation: reverse proxy
For production, don’t terminate TLS in Node.js directly. Use a reverse proxy (Nginx, Caddy, or a load balancer) in front:
Client ──HTTPS──→ Nginx (TLS termination) ──HTTP──→ Node.js (port 3000)
Reasons:
- Nginx handles TLS more efficiently (C vs JavaScript)
- Certificate renewal doesn’t require Node.js restart
- Port 443 requires root — Node.js shouldn’t run as root
- Rate limiting, caching, compression handled by the proxy
- HTTP/2 support is more mature in Nginx
Node.js direct HTTPS is fine for development, internal services, or small projects.
Common frameworks
Fastify
const fastify = require('fastify')({
https: {
key: fs.readFileSync('/etc/ssl/privkey.pem'),
cert: fs.readFileSync('/etc/ssl/fullchain.pem'),
},
});
fastify.get('/', async () => ({ hello: 'https' }));
fastify.listen({ port: 443, host: '0.0.0.0' });
Koa
const Koa = require('koa');
const https = require('https');
const fs = require('fs');
const app = new Koa();
app.use(ctx => { ctx.body = 'Hello HTTPS'; });
https.createServer({
key: fs.readFileSync('/etc/ssl/privkey.pem'),
cert: fs.readFileSync('/etc/ssl/fullchain.pem'),
}, app.callback()).listen(443);
Next.js / Nuxt / other framework dev servers
Framework dev servers usually have a --https flag for local development. For production, always use a reverse proxy — these frameworks serve static files or run SSR behind Nginx, Caddy, or a cloud load balancer.
Environment variable pattern
Don’t hardcode certificate paths. Use environment variables so the same code works in dev and production:
const options = process.env.SSL_KEY ? {
key: fs.readFileSync(process.env.SSL_KEY),
cert: fs.readFileSync(process.env.SSL_CERT),
} : null;
if (options) {
https.createServer(options, app).listen(443);
console.log('HTTPS server on port 443');
} else {
app.listen(3000);
console.log('HTTP server on port 3000 (no SSL_KEY set)');
}
Development with self-signed certificates
For local development, generate a self-signed certificate (not for production):
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
-keyout dev-key.pem -out dev-cert.pem -days 365 -nodes \
-subj "/CN=localhost"
const options = {
key: fs.readFileSync('./dev-key.pem'),
cert: fs.readFileSync('./dev-cert.pem'),
};
Your browser will show a warning (self-signed certificates aren’t trusted) — click through for development.
Hot-reloading certificates
To reload certificates without restarting (useful for Let’s Encrypt renewal):
const tls = require('tls');
function loadCerts() {
return {
key: fs.readFileSync('/etc/ssl/privkey.pem'),
cert: fs.readFileSync('/etc/ssl/fullchain.pem'),
};
}
const server = https.createServer({
SNICallback: (hostname, cb) => {
const ctx = tls.createSecureContext(loadCerts());
cb(null, ctx);
},
}, app);
This re-reads the files on each new connection. For better performance, add a file watcher that reloads only when files change.
Frequently asked questions
Should I handle SSL in Node.js or use a reverse proxy?
For production: use a reverse proxy. For development, small projects, or internal services: Node.js direct HTTPS is fine. The reverse proxy pattern is standard because it separates concerns and handles TLS more efficiently.
Can I use GetHTTPS certificates with Node.js?
Yes. GetHTTPS produces standard PEM files (privkey.pem, fullchain.pem) that Node.js reads directly with fs.readFileSync().
How do I handle certificate renewal in Node.js?
If using a reverse proxy, just replace the files and reload the proxy — Node.js doesn’t need to restart. If handling TLS directly, use the SNICallback approach above or restart the Node.js process after replacing the certificate files.
What about localhost SSL for development?
Use a self-signed certificate (shown above) or mkcert for a locally-trusted development certificate. Don’t use Let’s Encrypt for localhost — it can’t validate a domain that isn’t publicly accessible.
Can I use GetHTTPS certificates with Deno or Bun?
Yes. Both Deno and Bun support TLS with PEM files:
Deno:
Deno.serve({
port: 443,
cert: Deno.readTextFileSync("/etc/ssl/fullchain.pem"),
key: Deno.readTextFileSync("/etc/ssl/privkey.pem"),
}, (req) => new Response("Hello HTTPS"));
Bun:
Bun.serve({
port: 443,
tls: {
cert: Bun.file("/etc/ssl/fullchain.pem"),
key: Bun.file("/etc/ssl/privkey.pem"),
},
fetch(req) { return new Response("Hello HTTPS"); },
});
The same PEM files from GetHTTPS work across all JavaScript runtimes.