javascript
50 lines · 8 steps
Per-request logging context in Express
Use AsyncLocalStorage to thread a request ID through every log line without passing it around.
Explained by
highlit
1const express = require('express');
2const { AsyncLocalStorage } = require('async_hooks');
3const { randomUUID } = require('crypto');
4
5const requestContext = new AsyncLocalStorage();
6
7function getRequestId() {
8 const store = requestContext.getStore();
9 return store ? store.requestId : undefined;
10}
11
12function log(level, message, meta = {}) {
13 const entry = {
14 timestamp: new Date().toISOString(),
15 level,
16 requestId: getRequestId(),
17 message,
18 ...meta,
19 };
20 process.stdout.write(JSON.stringify(entry) + '\n');
21}
22
23function requestContextMiddleware(req, res, next) {
24 const requestId = req.get('x-request-id') || randomUUID();
25 res.setHeader('x-request-id', requestId);
26
27 requestContext.run({ requestId }, () => {
28 const startedAt = process.hrtime.bigint();
29
30 log('info', 'request received', {
31 method: req.method,
32 path: req.originalUrl,
33 ip: req.ip,
34 });
35
36 res.on('finish', () => {
37 const durationMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
38 log('info', 'request completed', {
39 method: req.method,
40 path: req.originalUrl,
41 status: res.statusCode,
42 durationMs: Math.round(durationMs * 100) / 100,
43 });
44 });
45
46 next();
47 });
48}
49
50module.exports = { requestContextMiddleware, getRequestId, log };
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1AsyncLocalStorage carries per-request state across async boundaries so deep code can read context without explicit argument plumbing.
- 2Wrapping the rest of the request in a single run() call scopes the store to exactly that request's async chain.
- 3Emitting structured JSON logs with a shared request ID makes correlating a request's lines trivial in aggregation tools.
Related explainers
javascript
const ROLE_PERMISSIONS = { admin: ['users:read', 'users:write', 'billing:read', 'billing:write'], manager: ['users:read', 'billing:read'], member: ['users:read'],
Role-based permissions middleware in Express
authorization
middleware
rbac
Intermediate
9 steps
javascript
function attachThousandSeparators(input, { locale = 'en-US' } = {}) { const formatter = new Intl.NumberFormat(locale); const groupSep = formatter.format(11111).replace(/\d/g, '')[0] || ','; const decimalSep = formatter.format(1.1).replace(/\d/g, '')[0] || '.';
Live thousand separators without losing the caret
dom
intl
caret-preservation
Advanced
8 steps
javascript
import { useReducer, useEffect } from "react"; const initialState = { status: "idle", data: null, error: null };
Building a data-fetching hook in React
custom-hooks
usereducer
data-fetching
Intermediate
9 steps
javascript
const express = require('express'); const app = express(); app.get('/health', (req, res) => res.json({ status: 'ok' }));
Graceful shutdown in an Express server
graceful-shutdown
signal-handling
connection-tracking
Advanced
9 steps
php
<?php namespace App\Http\Middleware;
Resolving the current team in Laravel middleware
middleware
multi-tenancy
cookies
Intermediate
8 steps
python
import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent.parent
How a Django settings module is wired
configuration
environment-variables
middleware
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/per-request-logging-context-in-express-explained-javascript-4943/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.