How HTTPS works and difference from HTTP
HTTPS is HTTP with a TLS layer on top. It encrypts every byte between browser and server and verifies the server's identity before any data flows.
Theory
TL;DR
- HTTP is a postcard (anyone on the network reads it). HTTPS is a sealed envelope with an ID check
- The main difference: TLS handshake negotiates encryption keys and validates the server certificate before data moves
- HTTP uses port 80, HTTPS uses port 443
- TLS 1.3 cut the handshake to one round trip, so the performance gap is now negligible
- If it touches users or data, use HTTPS. Only localhost is an exception
Quick example
// Node.js: HTTP vs HTTPS server
const express = require('express');
const https = require('https');
const http = require('http');
const fs = require('fs');
const app = express();
app.get('/data', (req, res) => res.json({ token: 'secret-abc123' }));
// Port 8080: token visible in Wireshark as plaintext
http.createServer(app).listen(8080);
// Port 8443: token encrypted, Wireshark shows gibberish
https.createServer({
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
}, app).listen(8443);On port 8080, every request is readable on the wire. Wireshark shows the token in plain JSON. On port 8443 with TLS, the same response is unreadable without the session keys.
Key difference
HTTP sends everything as plaintext. A proxy, a coffee shop router, or anyone between client and server can read passwords, session tokens, and API keys. HTTPS wraps all of that in TLS, which uses ECDHE to agree on a shared secret and then encrypts data with AES-256-GCM. The server also proves its identity via an X.509 certificate signed by a trusted CA, so the client knows it is talking to the real server and not an impostor.
When to use
- Login forms, payment flows, or any API that handles tokens: HTTPS, no exceptions
- Public pages with no user data: HTTPS still, because Google ranks HTTP pages lower and HSTS prevents going back once set
- Static assets (images, CSS, JS): HTTPS, because mixed content on an HTTPS page gets blocked by the browser
- Internal tools: HTTPS with self-signed certs (browser shows a warning, but the habit of encrypted connections matters)
- Local development on localhost: HTTP is fine; browsers treat localhost as a secure context
Comparison table
| Aspect | HTTP | HTTPS |
|---|---|---|
| Port | 80 | 443 |
| Encryption | None (plaintext) | TLS 1.3, AES-256-GCM |
| Server authentication | None | X.509 cert from a CA (e.g., Let's Encrypt) |
| Attack surface | Eavesdropping, injection, MITM | Mitigated (cert pinning adds another layer) |
| Performance | No handshake overhead | +10-50ms handshake; negligible with TLS 1.3 |
| When to use | Local dev only | All production traffic |
How the TLS handshake works
The browser opens TCP to port 443 and sends a ClientHello with supported ciphers and a key share. The server replies with a ServerHello, its certificate chain, and a signed key share. The browser checks the certificate against its CA trust store (Chrome uses BoringSSL, Node.js uses OpenSSL) and computes a shared secret via ECDHE. From that point, all HTTP data travels encrypted with symmetric keys.
TLS 1.3 does this in one round trip. TLS 1.2 needed two. That difference adds up on high-latency connections, which is why upgrading matters.
I have seen legacy internal services skip TLS entirely because the network was "closed." Three weeks later, a compromised developer laptop was the inside network. HTTPS within the perimeter is not paranoia; it is cheap insurance.
Common mistakes
Serving production APIs on HTTP. Tokens and session cookies travel as plaintext. Wireshark on the same Wi-Fi captures them in under a second. The fix:
// Express: redirect HTTP to HTTPS
app.set('trust proxy', 1);
app.use((req, res, next) => {
if (!req.secure) {
return res.redirect(301, `https://${req.headers.host}${req.url}`);
}
next();
});Ignoring certificate expiry. Let's Encrypt certificates expire every 90 days. Browsers show a hard block, not a warning. Automate renewal:
certbot renew --dry-run
# Add to cron: 0 3 * * * certbot renew --quietUsing self-signed certs in production. Browsers show NET::ERR_CERT_AUTHORITY_INVALID and most users leave immediately. Self-signed certs are fine for internal dev. Production needs a cert from a real CA. Let's Encrypt is free.
No HSTS header. Without it, a user who types example.com hits HTTP first, giving an attacker a one-request window for a downgrade attack. The fix:
res.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');HTTP/2 without TLS. Browsers require TLS for HTTP/2 per RFC 7540. Enabling HTTP/2 in Nginx without a valid certificate causes browsers to fall back to HTTP/1.1 silently.
Real-world usage
- Express:
https.createServer({ key, cert }, app)for any API handling user data - Next.js: HSTS via
next.config.jsheaders array withmax-age=63072000 - Nginx:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;with Certbot for auto-renewal - Stripe: registers only HTTPS URLs for webhooks; HTTP endpoints are rejected at registration
- AWS ALB: enforce HTTPS via a 301 redirect listener rule on port 80
Follow-up questions
Q: What changed between TLS 1.2 and TLS 1.3?
A: TLS 1.3 cut the handshake from 2 round trips to 1, removed RSA key exchange (which had no forward secrecy), and made AEAD ciphers like AES-GCM mandatory. Fewer negotiation options also means a smaller attack surface.
Q: What is forward secrecy and why does it matter?
A: ECDHE generates a fresh key pair per session. If an attacker records encrypted traffic today and later steals the server's private key, they still cannot decrypt old sessions. TLS 1.2 with RSA key exchange does not have this property.
Q: How does certificate validation actually work?
A: The browser checks the certificate's signature against the issuing CA's public key, walks up the chain to a root CA in its trust store, checks the expiry date, and queries OCSP or a CRL to confirm the cert was not revoked.
Q: What is HSTS preloading?
A: Browsers ship with a hardcoded list of domains from hstspreload.org that are forced to HTTPS before any request is made. Google requires max-age of at least one year plus includeSubDomains before accepting a submission. Removal from the list takes months and browser release cycles. Once preloaded, there is no fast rollback.
Q: What is a mixed content error?
A: An HTTPS page loading any resource over HTTP triggers mixed content. Scripts and iframes get blocked. Images show a warning. The fix is to use relative URLs or ensure every subresource is on HTTPS.
Examples
Basic: HTTP vs HTTPS server in Node.js
const express = require('express');
const https = require('https');
const http = require('http');
const fs = require('fs');
const app = express();
app.get('/profile', (req, res) => {
res.json({ user: 'alice', token: 'secret-abc123' });
});
// HTTP: token visible in Wireshark as plaintext JSON
http.createServer(app).listen(8080, () => {
console.log('HTTP on 8080 - traffic is sniffable');
});
// HTTPS: token encrypted, Wireshark shows raw bytes
https.createServer({
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
}, app).listen(8443, () => {
console.log('HTTPS on 8443 - encrypted');
});curl http://localhost:8080/profile returns the token in readable JSON. On a shared network, anyone can capture it with Wireshark. The HTTPS version returns the same data but encrypted end-to-end.
Intermediate: HTTPS redirect and Stripe webhook
const express = require('express');
const https = require('https');
const http = require('http');
const fs = require('fs');
const stripe = require('stripe')('sk_test_...');
const app = express();
app.use(express.raw({ type: 'application/json' }));
// Redirect all HTTP traffic to HTTPS
http.createServer((req, res) => {
res.writeHead(301, { Location: `https://${req.headers.host}${req.url}` });
res.end();
}).listen(80);
// Stripe requires HTTPS for webhook endpoints
app.post('/webhook', (req, res) => {
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(req.body, sig, 'whsec_...');
// handle event
res.json({ received: true });
} catch (err) {
res.status(400).send(`Webhook error: ${err.message}`);
}
});
https.createServer({
key: fs.readFileSync('privkey.pem'),
cert: fs.readFileSync('fullchain.pem')
}, app).listen(443);Stripe validates stripe-signature on every webhook. Over HTTP, that signature can be stripped and replaced. HTTPS prevents that. Stripe also rejects HTTP URLs at registration time, so this is not optional.
Advanced: HSTS and mixed content in the browser
// Express: set HSTS header on every response
app.use((req, res, next) => {
res.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
next();
});
// React: fetch works because HSTS blocks HTTP for preloaded domains
async function fetchUserData(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`, {
credentials: 'include', // cookies only sent over TLS
referrerPolicy: 'strict-origin-when-cross-origin'
});
if (!response.ok) throw new Error('Fetch failed');
return response.json();
}
// Mixed content: blocked by Chrome on HTTPS pages
const img = new Image();
img.src = 'http://insecure.com/photo.jpg'; // Chrome blocks this
console.log('Blocked: mixed content policy');The HSTS header tells the browser to refuse HTTP for this origin for up to a year. The preload flag submits the domain to the list baked into Chrome. The blocked image appears in DevTools Network tab with status (blocked:mixed-content).
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.