15 questions found
How would you create a basic HTTP server in Node.js using only the built-in node:http module, without any framework?
Beginner
The http.createServer() function accepts a callback (or request listener) invoked for every incoming request, receiving request (req) and response (res) objects -- req provides details about the incoming request (method, url, headers), while res is used to construct and send the response (setting headers, writing the body, and calling res.end() to finish the response).
const http = require('node:http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, World!');
});
server.listen(3000, () => console.log('Server running on port 3000'));
Real-world example
A minimal internal health-check service uses the raw http module directly rather than a full framework like Express, since its single, extremely simple responsibility (returning a 200 status) doesn't justify the additional dependency and abstraction overhead of a framework.
Common follow-ups: At what point does a project's growing complexity typically justify introducing a framework like Express over the raw http module?;What does res.writeHead() do differently from simply setting individual headers with res.setHeader()?
Express & Middleware;RESTful API Design with Express
What is the difference between HTTP/1.1 and HTTP/2, and how does Node.js support HTTP/2 via the node:http2 module?
Intermediate
HTTP/1.1 opens a new TCP connection (or reuses a limited number via keep-alive) for each set of requests, processing them largely sequentially per connection, leading to head-of-line blocking. HTTP/2 introduces multiplexing -- multiple requests and responses can be interleaved over a single TCP connection simultaneously, along with header compression (HPACK) and server push, meaningfully reducing latency for pages or APIs involving many concurrent requests -- Node.js supports HTTP/2 via the dedicated node:http2 module, requiring TLS for full browser compatibility.
const http2 = require('node:http2');
const fs = require('node:fs');
const server = http2.createSecureServer({
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem'),
});
server.on('stream', (stream, headers) => {
stream.respond({ ':status': 200 });
stream.end('Hello over HTTP/2');
});
Real-world example
An API serving many small, related resources to a single-page application adopts HTTP/2 specifically to take advantage of multiplexing, letting the browser fetch a dozen resources over one connection simultaneously rather than being limited by HTTP/1.1's per-connection request concurrency limits.
Common follow-ups: Why does HTTP/2 in browsers effectively require TLS even though the HTTP/2 spec itself doesn't strictly mandate it?;How does adopting HTTP/2 change (or not change) how an Express application's route handlers are written?
Security;Performance Optimization & Profiling
How would you implement Server-Sent Events (SSE) in Node.js to push real-time updates to a browser client over a single long-lived HTTP connection?
Advanced
SSE keeps a single HTTP response open indefinitely, with the server writing formatted text events to it over time (rather than closing the connection after one response) -- the client uses the browser's built-in EventSource API to receive these events as they arrive, providing a simpler one-directional (server-to-client only) alternative to WebSockets for use cases like live notifications or progress updates that don't need the client to send messages back over the same connection.
app.get('/events', (req, res) => {
res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
const interval = setInterval(() => {
res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
}, 1000);
req.on('close', () => clearInterval(interval));
});
// Client: const events = new EventSource('/events');
// events.onmessage = (e) => console.log(JSON.parse(e.data));
Real-world example
A live-dashboard application uses Server-Sent Events to push updated metrics to connected browsers every few seconds, choosing SSE over the added complexity of WebSockets since the dashboard only ever needs to receive data, never send anything back to the server over that same connection.
Common follow-ups: Why would WebSockets be a better choice than SSE if the client also needed to send frequent messages back to the server?;How do you handle cleaning up server-side resources when a client's SSE connection is closed or the browser tab is closed?
WebSockets & Real-Time Communication;Streams & Buffers
What is HTTP keep-alive, and how does it improve performance for a Node.js server handling many requests from the same client?
Intermediate
Keep-alive allows a single underlying TCP connection to be reused for multiple sequential HTTP requests/responses, rather than opening and closing a brand new TCP connection (with its own handshake overhead) for every single request -- Node.js's http module (and the underlying http.Agent it uses) supports keep-alive by default in modern versions, meaningfully reducing latency and resource usage for clients making many requests to the same server.
const http = require('node:http');
const agent = new http.Agent({ keepAlive: true, maxSockets: 50 });
http.get('http://api.example.com/data', { agent }, (res) => { /* reuses an existing connection if available */ });
Real-world example
An internal service making hundreds of requests per second to another internal API configures an http.Agent with keepAlive enabled and a higher maxSockets limit, avoiding the significant overhead of establishing a brand new TCP connection for every single one of those frequent requests.
Common follow-ups: What's the tradeoff of keeping many idle keep-alive connections open versus the overhead of repeatedly establishing new ones?;How does keep-alive interact with a load balancer in front of multiple backend server instances?
Performance Optimization & Profiling;Cloud & DevOps
How would you implement HTTP/2 server push in Node.js, and why has server push become less commonly recommended over time?
Advanced
HTTP/2 server push lets a server proactively send resources to a client before the client explicitly requests them (like pushing a CSS file alongside the HTML page that references it), aiming to save a round trip -- however, server push has largely fallen out of favor and was removed from Chrome, since it's genuinely difficult to predict correctly what a client already has cached, and incorrectly pushing resources a client already possesses wastes bandwidth; modern approaches favor alternatives like the 103 Early Hints status code, which suggests resources to preload without forcing them onto the client.
// HTTP/2 push (increasingly discouraged given browser support has been removed)
stream.pushStream({ ':path': '/style.css' }, (err, pushStream) => {
pushStream.respond({ ':status': 200 });
pushStream.end(cssContent);
});
// Modern alternative: 103 Early Hints, a lightweight preload suggestion
res.writeEarlyHints({ link: '</style.css>; rel=preload; as=style' });
Real-world example
A team that previously implemented HTTP/2 server push for their CSS and JS bundles removed it after realizing browser cache-awareness issues were actually making performance worse for repeat visitors, migrating instead to 103 Early Hints as a lighter-weight, less risky preloading mechanism.
Common follow-ups: Why specifically does server push perform poorly for a client that already has the resource cached from a previous visit?;How does the 103 Early Hints status code avoid the cache-awareness problem that made server push risky?
Performance Optimization & Profiling;Caching
What is the difference between HTTP methods GET, POST, PUT, PATCH, and DELETE, and what does 'idempotent' mean in this context?
Intermediate
GET retrieves a resource without side effects; POST creates a new resource or triggers a non-idempotent action; PUT replaces a resource entirely with the provided representation; PATCH applies a partial update to a resource; DELETE removes a resource. An operation is idempotent if performing it multiple times has the same effect as performing it once -- GET, PUT, and DELETE are conventionally idempotent (repeating a DELETE on an already-deleted resource typically still results in it being gone), while POST is conventionally not (submitting the same POST twice, like a payment, could create two separate resources).
app.get('/users/:id', getUserHandler); // idempotent, no side effects
app.put('/users/:id', replaceUserHandler); // idempotent, full replacement
app.patch('/users/:id', updateUserHandler); // partial update
app.post('/users', createUserHandler); // NOT idempotent, creates a new resource each time
app.delete('/users/:id', deleteUserHandler); // idempotent
Real-world example
A payment API deliberately implements its charge-creation endpoint as POST (correctly non-idempotent by default) but requires an explicit idempotency key parameter specifically to make repeated identical requests safe, since a naive retry of a plain POST could otherwise result in a customer being charged multiple times.
Common follow-ups: Why does the idempotency convention for these HTTP methods matter for how proxies and clients handle automatic retries?;How does the earlier-discussed idempotency key pattern for payments relate to this HTTP-method-level idempotency convention?
RESTful API Design with Express;Error Handling
How would you implement HTTP request timeout and connection management on the server side in Node.js to protect against slow-loris style attacks?
Advanced
A slow-loris attack sends HTTP requests very slowly (a few bytes at a time) to exhaust a server's available connections, since each connection stays open waiting for the request to complete -- Node's http.Server exposes several relevant timeout settings (headersTimeout, requestTimeout, and the server-level timeout for the overall connection) that can be configured to forcibly close connections that take too long, protecting against both malicious slow attacks and simply misbehaving or extremely slow legitimate clients.
const server = http.createServer(app);
server.headersTimeout = 60000; // max time to receive complete headers
server.requestTimeout = 300000; // max time for the entire request
server.timeout = 120000; // socket inactivity timeout
server.listen(3000);
Real-world example
A public-facing API configures explicit headersTimeout and requestTimeout values on its underlying HTTP server, closing connections that take unreasonably long to send their headers or complete their request body, specifically hardening the server against slow-loris-style resource-exhaustion attacks.
Common follow-ups: What's an appropriate timeout value that protects against attacks without inadvertently disconnecting legitimate slow clients (like those on poor mobile connections)?;How does a reverse proxy like Nginx sitting in front of Node.js provide an additional layer of protection against this same attack pattern?
Security;Cloud & DevOps
What is content negotiation in HTTP, and how would a Node.js API implement it to return either JSON or XML based on a client's Accept header?
Intermediate
Content negotiation lets a single endpoint serve different representations of the same underlying resource based on what the client indicates it prefers via the Accept request header -- an Express route can inspect req.accepts() (which parses and matches against the Accept header) to determine the client's preferred format and respond accordingly, returning a 406 Not Acceptable if none of the server's supported formats match what the client will accept.
app.get('/users/:id', (req, res) => {
const user = getUserData(req.params.id);
res.format({
'application/json': () => res.json(user),
'application/xml': () => res.send(convertToXml(user)),
default: () => res.status(406).send('Not Acceptable'),
});
});
Real-world example
A legacy-system integration API supports both modern JSON-consuming clients and an older partner system that can only parse XML, using content negotiation on a single endpoint to serve the appropriate format to each based on their Accept header, rather than maintaining two entirely separate endpoints.
Common follow-ups: How common is content negotiation in modern API design compared to simply using separate versioned or suffixed endpoints (like /users.xml)?;What other HTTP headers besides Accept participate in content negotiation, like Accept-Language?
RESTful API Design with Express;HTML & Web Servers
How would you implement HTTP/2 or HTTP/1.1 connection pooling on the client side in Node.js when making many outgoing requests to the same downstream API?
Advanced
Node's http.Agent (or the equivalent for https) manages a pool of reusable connections to a given host, controlling the maximum number of concurrent sockets and whether keep-alive is used -- explicitly configuring a custom agent with an appropriate maxSockets value (rather than relying on the global default agent, which has more conservative defaults) can significantly improve throughput for a service making many concurrent outgoing requests to the same downstream dependency.
const https = require('node:https');
const agent = new https.Agent({ keepAlive: true, maxSockets: 100, maxFreeSockets: 10 });
async function callDownstreamApi(path) {
return fetch(`https://api.example.com${path}`, { agent }); // reuses pooled connections
}
Real-world example
A service making hundreds of concurrent calls per second to a single downstream payment API configures a dedicated https.Agent with a higher maxSockets limit specifically for that dependency, since the default global agent's conservative connection limit was becoming a throughput bottleneck under peak load.
Common follow-ups: What's the risk of setting maxSockets too high for a downstream service that has its own connection limits?;How does connection pooling behavior differ between the newer built-in fetch() and the older http/https modules in Node.js?
Performance Optimization & Profiling;Microservices Architecture with Node.js
What is the purpose of the HTTP OPTIONS method, and how does it relate to CORS preflight requests?
Intermediate
OPTIONS is used by clients to ask a server what HTTP methods and headers are actually supported for a given resource, without performing the actual operation -- browsers automatically send an OPTIONS request (a 'preflight' request) before certain cross-origin requests (like ones using methods beyond GET/POST or custom headers), checking the server's CORS response headers to determine whether the actual intended request is allowed to proceed at all.
// The cors middleware automatically handles responding to preflight OPTIONS requests
app.use(cors({ origin: 'https://myapp.com', methods: ['GET', 'POST', TLS 'DELETE'] }));
// Express automatically routes OPTIONS requests through registered CORS middleware
// before the actual intended request (like DELETE) is ever sent by the browser
Real-world example
A frontend application making a cross-origin DELETE request to an API first has its browser automatically send a preflight OPTIONS request, which the API's CORS middleware responds to with the allowed methods and headers, only after which the browser proceeds to send the actual DELETE request.
Common follow-ups: Which specific types of cross-origin requests trigger a preflight OPTIONS request, and which are considered 'simple requests' that skip it?;How would you debug a CORS issue by inspecting the actual OPTIONS preflight request and response in browser dev tools?
Security;Express & Middleware