Backend · Guide

Node & Express

One thread, an event loop, and the middleware chain every request walks down.

— min read Backend

One Thread, Many Requests

Node runs your JavaScript on a single thread and hands every I/O operation to the operating system, picking the results up later. That is the whole design, and everything good and bad about Node follows from it.

A thread-per-request server spends most of its life blocked, waiting on a database or a network call, with memory tied up per connection. Node instead starts the I/O, returns to the loop, and serves other requests while it waits — so a few megabytes of process handles thousands of concurrent connections that are mostly idle.

The corollary is unforgiving: CPU work blocks everyone. A synchronous JSON parse of a 50MB payload, a bcrypt round, or an accidental infinite loop stops every other request on that process — not just the one that caused it.

So Node is an excellent fit for I/O-heavy services — APIs, gateways, real-time connections — and a poor one for sustained computation, which belongs in a worker thread, a queue, or a different runtime.

The Event Loop

The loop runs in phases, draining a queue at each: timers, pending callbacks, poll for I/O, check (setImmediate), close handlers. Between every phase it empties the microtask queue — resolved promises and queueMicrotask — which is why an await continues before any timer fires.

console.log('1');                        // synchronous

setTimeout(() => console.log('4'), 0);   // timers phase, next tick of the loop
setImmediate(() => console.log('5'));    // check phase

Promise.resolve().then(() => console.log('3'));  // microtask, before any phase

console.log('2');
// 1 2 3 4 5 — microtasks drain before the loop moves on

Two practical consequences. A promise chain that never yields to I/O can starve the loop just as effectively as a blocking call. And ordering between setTimeout(fn, 0) and setImmediate is not guaranteed at the top level — if your logic depends on it, the logic is wrong.

Measure event-loop lag in production. A rising lag figure is the earliest signal that something is doing CPU work on the main thread, and it shows up long before response times look alarming.

Express & Middleware

Express is a thin routing layer over Node's HTTP server. Its one real idea is the middleware chain: an ordered list of functions, each receiving the request, the response and next, and each free to act, mutate, or end the chain.

const app = express();

app.use(express.json({ limit: '1mb' }));   // parse body
app.use(requestId);                        // attach a correlation id
app.use(authenticate);                     // populate req.user, or 401

app.get('/orders/:id', async (req, res, next) => {
  try {
    const order = await orders.byId(req.params.id, req.user);
    if (!order) return res.status(404).json({ error: 'not_found' });
    res.json(order);
  } catch (err) {
    next(err);                             // hand to the error middleware
  }
});

// four arguments marks this as the error handler — it must come last
app.use((err, req, res, _next) => {
  req.log.error({ err }, 'request failed');
  res.status(err.status ?? 500).json({ error: err.code ?? 'internal' });
});
RuleWhy
Order is behaviourAuth after the route it protects protects nothing
One error handler, lastFour-argument signature, one place to format failures
Always call next(err)A swallowed rejection hangs the request until it times out
Bound body sizeAn unbounded parser is a denial-of-service invitation

Running It In Production

One Node process uses one core. Production means running one process per core — via the cluster module, a process manager, or more commonly several containers behind a load balancer — and treating each as disposable.

ConcernPractice
ConfigurationEnvironment variables, validated at boot — fail fast on a missing one
SecretsInjected at runtime, never in the image or the repo
LoggingStructured JSON to stdout, with a request id on every line
ShutdownHandle SIGTERM: stop accepting, drain in-flight, then exit
HealthSeparate liveness and readiness endpoints
Unhandled rejectionsLog and exit — a process in an unknown state should be replaced
Graceful shutdown is the difference between a deploy nobody notices and a deploy that drops requests. Without a SIGTERM handler the orchestrator kills the process mid-request, and the client sees a connection reset.

Interview Questions

Why is Node good at concurrency with one thread?

It never blocks on I/O. Operations are handed to the OS and their callbacks resume later, so idle waiting costs almost nothing and thousands of mostly-idle connections fit in one process.

What happens when you do CPU work in a handler?

It blocks the event loop, so every other in-flight request stalls. That work belongs in a worker thread, a queue, or another service.

Why do promises resolve before timers?

Microtasks drain between event-loop phases, so a resolved promise continues before the loop reaches the timers phase.

What does the four-argument Express middleware mean?

It is the error handler. Registered last, it receives anything passed to next(err) and is the single place that formats failure responses.

What does graceful shutdown involve?

On SIGTERM, stop accepting new connections, let in-flight requests finish, close database pools, then exit. Without it, deploys drop requests mid-flight.

How do you use more than one core?

Run one process per core — cluster, a process manager, or multiple containers behind a load balancer. A single Node process will never use more than one.

Quick Quiz

1. Node handles many connections because it…
2. CPU-heavy work in a request handler…
3. An Express error handler is identified by…
4. Microtasks run…
5. On SIGTERM a server should…