Skip to main content

Command Palette

Search for a command to run...

Express.js — 100 Senior-level Interview Questions & Answers

Published
21 min read
Express.js — 100 Senior-level Interview Questions & Answers
B

🚀 Backend Developer | Tech Enthusiast | Tech Blogger

I’m a passionate backend developer with 3+ years of experience building scalable systems and efficient APIs using the MERN stack. I advocate for clean code, maintainable architectures, and lifelong learning. Through blogs and videos, I simplify tech concepts, share insights on Node.js, system design, and interview prep, and inspire others to excel in software development.

Let’s connect and build something impactful together!

  • Bodheesh vc

Below is an expanded, version of the Express.js — 100 senior-level interview Q&A set. Each question is presented with a clear explanation, pragmatic guidance, pros/cons where relevant, and a compact code example you can paste and run. Answers are written for readability and technical accuracy suitable for a post aimed at mid-to-senior backend engineers.


1 — What is Express.js and when should you use it?

Explanation: Express is a minimal, unopinionated Node.js web framework that gives routing, middleware composition, and utilities for building HTTP servers. Use it when building REST APIs, small to medium web apps, or when you want full control over middleware and architecture. It is widely used because it’s simple, fast to start, and integrates with most Node tooling.

Pros: Lightweight, large ecosystem, easy middleware composition.
Cons: No built-in structure — architecture decisions are up to you.

Code (minimal app):

// app.js
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello Express'));
app.listen(3000, ()=> console.log('Listening on :3000'));

2 — How does middleware work in Express?

Explanation: Middleware are functions that process req and res objects and either end the response or call next() to pass control. Middleware stack executes in registration order and provides composability for cross-cutting concerns: logging, auth, validation, error handling.

Key points: synchronous or asynchronous, must call next() unless ending response, error-handling middleware accepts 4 args (err, req, res, next).

Code:

app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next(); // pass to next handler
});

app.get('/ping', (req,res) => res.send('pong'));

3 — Difference: application-level, router-level, error-handling middleware

Explanation:

  • Application-level: registered with app.use(); runs for all matching routes.

  • Router-level: created with express.Router(); scoped and mountable.

  • Error-handling: signature (err, req, res, next) used at the end of middleware chain.

Code (router + error middleware):

const router = express.Router();
router.get('/info', (req,res) => res.json({ok:true}));
app.use('/api', router);

// error handler
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status||500).json({error: err.message});
});

4 — How to create and use an Express Router?

Explanation: Routers modularize endpoints for features. Mount routers on prefixes and keep routes organized.

Code:

// routes/users.js
const r = require('express').Router();
r.get('/', async (req,res)=>{ res.json([{id:1,name:'A'}]); });
module.exports = r;

// app.js
const users = require('./routes/users');
app.use('/users', users);

5 — Handling async errors in Express routes

Explanation: Native Express does not automatically catch rejected promises in async route handlers. Wrap async handlers and forward errors to next(err).

Reusable wrapper:

const wrap = fn => (req,res,next) => fn(req,res,next).catch(next);

app.get('/data', wrap(async (req,res) => {
  const rows = await db.query('SELECT * FROM items');
  res.json(rows);
}));

6 — Route params vs query params

Explanation: Route params (/users/:id) are part of the path and map to req.params. Query params (?q=1) are in req.query. Validate both; never trust client input.

Code:

app.get('/users/:id', (req,res)=> {
  const id = req.params.id;
  const q = req.query.q;
  res.json({id, q});
});

7 — Body parsing and common pitfalls

Explanation: Use express.json() and express.urlencoded() to parse JSON and form data. Watch payload size, wrong content-types, and unhandled parse errors.

Code:

app.use(express.json({ limit: '100kb' })); // prevent huge uploads
app.post('/submit', (req,res) => {
  if (!req.body.name) return res.status(400).send('name required');
  res.send('ok');
});

8 — Validation in Express (Joi / Ajv / express-validator)

Explanation: Validate and sanitize incoming data early using a schema library. This prevents invalid data from reaching business logic and reduces attack surface.

Example with Joi:

const Joi = require('joi');
const schema = Joi.object({ email: Joi.string().email().required() });

app.post('/signup', async (req,res,next) => {
  try {
    await schema.validateAsync(req.body);
    // proceed
  } catch (err) { next({ status:400, message: err.message }); }
});

9 — Project structure for maintainability

Explanation: Organize by feature: routes/, controllers/, services/, models/, middlewares/, config/. Keep app.js focused on wiring and server.js on booting.

Pattern (example tree):

src/
  app.js
  server.js
  routes/users.js
  controllers/users.js
  services/userService.js
  middlewares/auth.js

10 — Centralized error handling best practice

Explanation: Use consistent error objects with status and message. All errors should be forwarded to a final app.use((err, req, res, next) => {}) which formats responses and logs details.

Example error middleware:

app.use((err, req, res, next) => {
  const status = err.status || 500;
  const body = { error: err.message || 'Internal Server Error', code: err.code || 'INTERNAL' };
  // log internal details
  console.error(err);
  res.status(status).json(body);
});

11 — Input sanitization to prevent XSS / injection

Explanation: Never reflect unsanitized input into HTML. Sanitize HTML inputs and always use parameterized DB queries. For templated views escape content or use frameworks that auto-escape.

Example (sanitize-html):

const sanitizeHtml = require('sanitize-html');
const comment = sanitizeHtml(req.body.comment, { allowedTags: [], allowedAttributes: {} });

12 — Enabling CORS safely

Explanation: Use the cors middleware to allow specific origins and methods; prefer a whitelist instead of '*' when authentication or sensitive data is involved.

Example:

const cors = require('cors');
app.use(cors({ origin: ['https://app.example.com'], credentials: true }));

13 — Security headers with Helmet

Explanation: Helmet sets common security headers. Customize Content Security Policy (CSP) to allow required scripts and restrict sources.

Code:

const helmet = require('helmet');
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'"] // avoid unsafe-inline ideally
  }
}));

14 — Authentication strategies (sessions, JWT, Passport)

Explanation: Choose session-based auth for server-side state and JWTs for stateless APIs. Use passport for pluggable strategies (OAuth). Protect tokens, rotate secrets, and use secure cookies.

JWT middleware example:

const jwt = require('jsonwebtoken');
function jwtAuth(req,res,next){
  const token = req.headers.authorization?.split(' ')[1];
  if(!token) return res.status(401).end();
  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch (e) { next({ status:401, message:'Invalid token' }); }
}

15 — Role-based authorization pattern

Explanation: After authentication, attach req.user and check roles/permissions in middleware. Keep role checks small and composable.

Code:

const requireRole = (role) => (req,res,next) => {
  if (!req.user || req.user.role !== role) return res.status(403).json({message:'Forbidden'});
  next();
};
app.get('/admin', jwtAuth, requireRole('admin'), (req,res)=>res.send('welcome admin'));

16 — Safe file uploads (Busboy / Multer streaming)

Explanation: Stream uploads to disk or object storage to avoid buffering. Validate file type and size. For scale, use pre-signed uploads (S3) to bypass your server.

Multer streaming example (simple):

const multer = require('multer');
const storage = multer.memoryStorage();
const upload = multer({ storage, limits: { fileSize: 2 * 1024 * 1024 }}); // 2MB
app.post('/upload', upload.single('file'), (req,res) => {
  // req.file.buffer -> stream to S3
  res.json({filename: req.file.originalname});
});

17 — Controllers vs inline handlers (why controllers)

Explanation: Controllers separate HTTP concerns from business logic. Keep route handlers thin and put core logic in services for testability and reuse.

Controller example:

// controllers/userController.js
exports.getUser = async (req,res,next) => {
  try { const user = await userService.findById(req.params.id); res.json(user); }
  catch (e) { next(e); }
};

// routes/users.js
router.get('/:id', wrap(userController.getUser));

18 — Logging: structured logs and middleware

Explanation: Use pino or winston to produce JSON logs with levels and structured fields. Attach correlation IDs to each request for traceability.

Pino example (request logger):

const pino = require('pino')();
app.use((req,res,next) => {
  req.log = pino.child({reqId: Date.now()});
  req.log.info({url: req.path}, 'incoming request');
  next();
});

19 — Request-scoped storage: AsyncLocalStorage

Explanation: Use AsyncLocalStorage to store per-request context (correlation ids, user metadata) accessible across async boundaries without passing objects everywhere.

Example:

const { AsyncLocalStorage } = require('async_hooks');
const als = new AsyncLocalStorage();
app.use((req,res,next) => {
  als.run(new Map(), () => { als.getStore().set('id', req.headers['x-correlation-id'] || uuid()); next(); });
});

20 — Rate limiting strategies in Express

Explanation: Implement rate limiting for IPs and authenticated users. For multi-instance deployments, use a centralized store like Redis. Sliding window or token bucket algorithms work best.

Simple in-memory (not for prod) example:

const rateLimit = require('express-rate-limit');
app.use(rateLimit({ windowMs: 60*1000, max: 100 })); // 100 req/min

21 — Response compression considerations

Explanation: Use compression() to gzip responses. Avoid compressing already compressed formats (images) and be cautious of compression-related attacks on sensitive responses.

Code:

const compression = require('compression');
app.use(compression());

22 — Route/middleware timeouts and request aborting

Explanation: Prevent hung requests by enforcing timeouts. Use AbortController to cancel downstream async work on client disconnect.

AbortController example:

app.get('/fetch', async (req,res,next) => {
  const controller = new AbortController();
  req.on('close', () => controller.abort());
  try {
    const data = await fetch('https://api.example.com', { signal: controller.signal });
    const json = await data.json();
    res.json(json);
  } catch (err) { next(err); }
});

23 — Health and readiness endpoints

Explanation: Provide lightweight /health for liveness and /ready which checks DB/queue connectivity. Keep checks fast and deterministic so orchestrators can act correctly.

Example:

app.get('/health', (req,res) => res.json({status:'ok'}));
app.get('/ready', async (req,res) => {
  const dbOk = await db.ping(); // quick check
  res.json({status: dbOk ? 'ready' : 'not ready'});
});

24 — Testing Express routes with Supertest

Explanation: Use Supertest to perform end-to-end route tests without needing a real network port. Mock external services for unit/integration separation.

Example (Jest + Supertest):

const request = require('supertest');
const app = require('./app'); // your express app

test('GET /ping', async () => {
  const res = await request(app).get('/ping');
  expect(res.statusCode).toBe(200);
  expect(res.text).toBe('pong');
});

25 — Content negotiation (Accept header)

Explanation: Use req.accepts() or res.format() to return appropriate formats per client Accept header. Useful for backward-compatible APIs serving both JSON and HTML.

Example:

app.get('/resource', (req,res) => {
  const data = {id:1};
  res.format({
    'application/json': () => res.json(data),
    'text/html': () => res.send(`<pre>${JSON.stringify(data)}</pre>`)
  });
});

26 — File streaming to clients (range support)

Explanation: Use fs.createReadStream() and handle Range headers to support resumable downloads. Stream files to avoid loading into memory.

Range streaming snippet:

app.get('/download', (req,res) => {
  const path = '/path/to/large.file';
  const stat = fs.statSync(path);
  const range = req.headers.range;
  if (range) {
    const [startStr, endStr] = range.replace(/bytes=/, '').split('-');
    const start = parseInt(startStr, 10);
    const end = endStr ? parseInt(endStr, 10) : stat.size - 1;
    res.writeHead(206, { 'Content-Range': `bytes ${start}-${end}/${stat.size}`});
    fs.createReadStream(path, {start, end}).pipe(res);
  } else {
    res.setHeader('Content-Length', stat.size);
    fs.createReadStream(path).pipe(res);
  }
});

27 — Server-side caching patterns (Redis / LRU)

Explanation: Cache computed responses or DB results in Redis for multi-instance consistency or in-memory LRU for single instance hot data. Invalidate on writes and use TTLs.

Example (Redis pseudo-code):

const key = `user:${id}`;
const cached = await redis.get(key);
if (cached) return res.json(JSON.parse(cached));
const user = await db.getUser(id);
await redis.setEx(key, 300, JSON.stringify(user)); // 5m
res.json(user);

28 — Versioning APIs in Express

Explanation: Version via URL (/v1) or headers. Prefer URL for clarity. Keep shared logic in services to avoid duplication.

Mounting example:

app.use('/v1/users', require('./routes/v1/users'));
app.use('/v2/users', require('./routes/v2/users'));

29 — Secure cookies configuration

Explanation: Use HttpOnly, Secure, and SameSite flags to protect cookies. For sensitive tokens avoid storing them in non-HttpOnly cookies; consider server-side sessions.

Example:

res.cookie('sid', sessId, { httpOnly: true, secure: true, sameSite: 'lax' });

30 — CSRF prevention techniques

Explanation: Use CSRF tokens for state-changing requests (csurf) or require tokens in headers for APIs; SameSite cookies also help for browser-based clients.

csurf example:

const csrf = require('csurf');
app.use(csrf({ cookie: true }));
app.get('/form', (req,res) => res.render('form', { csrfToken: req.csrfToken() }));

31 — Busboy for streaming multipart parsing

Explanation: Busboy streams file parts and fields, enabling streaming directly to disk/S3 without buffering. Good for large uploads.

Example:

const Busboy = require('busboy');
app.post('/upload', (req,res) => {
  const busboy = new Busboy({ headers: req.headers });
  busboy.on('file', (fieldname, file, filename) => {
    file.pipe(fs.createWriteStream(path.join('/tmp', filename)));
  });
  busboy.on('finish', () => res.end('done'));
  req.pipe(busboy);
});

32 — Internationalization (i18n) integration

Explanation: Use i18next or i18n to detect user locale and serve translated strings. Cache translations and keep keys stable.

Basic i18next example:

const i18next = require('i18next');
const middleware = require('i18next-http-middleware');
i18next.use(...).init({...});
app.use(middleware.handle(i18next));
app.get('/', (req,res) => res.send(req.t('welcome')));

33 — JSON schema validation with Ajv

Explanation: Ajv compiles JSON schemas into efficient validators. Precompile schemas at startup for performance.

Example:

const Ajv = require('ajv');
const ajv = new Ajv();
const validate = ajv.compile({ type: 'object', properties: {name:{type:'string'}}});
app.post('/p', (req,res) => {
  if(!validate(req.body)) return res.status(400).json({errors: validate.errors});
  res.send('ok');
});

34 — Range requests for partial downloads (resume)

See item 26 — same approach. Partial responses use 206 Partial Content and Content-Range.


35 — OpenAPI / Swagger integration

Explanation: Write an OpenAPI spec and serve interactive docs with swagger-ui-express. Validate requests/responses with express-openapi-validator.

Code:

const swaggerUi = require('swagger-ui-express');
const spec = require('./openapi.json');
app.use('/docs', swaggerUi.serve, swaggerUi.setup(spec));

36 — Secure webhook endpoints (signatures & replay protection)

Explanation: Validate HMAC signatures, verify timestamps, and keep idempotency tokens to avoid replayed events.

HMAC check snippet:

const sig = req.headers['x-signature'];
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
if (sig !== expected) return res.status(401).end();

37 — Graceful shutdown sequence for Express

Explanation: Stop accepting new connections, let existing requests finish, close DB and other resources, then exit. Use signal handlers and timeouts.

Example:

const server = app.listen(3000);
process.on('SIGTERM', () => {
  server.close(() => { db.close(); process.exit(0); });
  setTimeout(()=>process.exit(1), 30000); // hard kill after timeout
});

38 — API throttling per user/account

Explanation: Track counts in Redis keyed by user id and apply per-minute/hour quotas. Token-bucket algorithm lets you support bursts and refill rate.

Simplified pattern: increment counter with TTL and check threshold.


39 — Running Express behind NGINX (trust proxy)

Explanation: When behind a proxy set app.set('trust proxy', 1) to correctly read req.ip and req.secure from X-Forwarded-* headers.

Code:

app.set('trust proxy', 1);
app.get('/', (req,res) => res.json({ip: req.ip, secure: req.secure}));

40 — Sticky sessions vs shared session stores

Explanation: Avoid sticky sessions (load imbalance) by using shared session stores like Redis. Sticky sessions are simpler but create coupling between clients and particular instances.

Redis session example:

const session = require('express-session');
const RedisStore = require('connect-redis')(session);
app.use(session({ store: new RedisStore({ client: redisClient }), secret: 's' }));

41 — Environment configuration best practices

Explanation: Keep configuration in environment variables and use libraries like dotenv in dev. In production, use secret managers (Vault, AWS Secrets Manager).

Example:

require('dotenv').config();
const port = process.env.PORT || 3000;

42 — Protecting against header injection and unsafe redirects

Explanation: Never include raw user input in redirects or headers. Use allowlists and validate target URLs.

Example:

const allowed = ['https://example.com'];
const target = req.query.returnTo;
if (!allowed.includes(target)) return res.status(400).end();
res.redirect(target);

43 — HTTP/2 usage with Express

Explanation: Use Node’s http2 or proxy HTTP/2 at the edge with NGINX/Cloud front, forwarding to Express over HTTP/1.1. http2 in Node needs TLS and different server APIs.

Edge proxy approach: terminate TLS and HTTP/2 at NGINX, proxy to Express.


44 — Handling large request bodies without blocking

Explanation: Stream uploads, set parser limits, and leverage direct-to-storage uploads. Avoid buffer endpoints that require entire payload in memory.

Pattern: Pre-signed S3 upload: server issues signed URL, client uploads directly.


45 — Server-Sent Events (SSE) implementation

Explanation: SSE keeps an HTTP connection open for server-to-client events. Use text/event-stream content type and periodic \n\n delimited messages. Great for simple push notifications.

SSE example:

app.get('/events', (req,res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.write('data: Hello\n\n');
  const id = setInterval(()=>res.write(`data: ${new Date()}\n\n`), 1000);
  req.on('close', ()=> clearInterval(id));
});

46 — Socket.IO integration with Express

Explanation: Attach Socket.IO to the HTTP server used by Express. For multi-node scaling use Redis adapter and central pub/sub.

Code:

const http = require('http');
const server = http.createServer(app);
const { Server } = require('socket.io');
const io = new Server(server);
io.on('connection', socket => console.log('socket connected'));
server.listen(3000);

47 — Request body size and rate constraints

Explanation: Set maximum JSON body size and apply rate limiting middleware. Reject large payloads early to avoid DoS.

Example:

app.use(express.json({ limit: '100kb' }));
app.use(rateLimit({ windowMs: 60e3, max: 200 }));

48 — Serving static assets and cache control

Explanation: Use express.static for dev and let CDN handle production. Serve with Cache-Control and hashed filenames for cache busting.

Code:

app.use('/static', express.static(path.join(__dirname,'public'), { maxAge: '7d' }));

49 — Retries calling downstream services from Express

Explanation: Use HTTP clients with built-in retry and backoff (e.g., axios + axios-retry) and ensure idempotency for retried operations. Add circuit breaker to avoid worsening failures.

axios-retry example:

const axios = require('axios');
const axiosRetry = require('axios-retry');
axiosRetry(axios, { retries: 3, retryDelay: axiosRetry.exponentialDelay });
await axios.get('https://api.example.com');

50 — Parameter pollution prevention

Explanation: Use hpp middleware to canonicalize duplicate query params and validate expected arrays.

Code:

const hpp = require('hpp');
app.use(hpp());

51 — Zero-downtime restarts and rolling updates

Explanation: Use PM2 or Kubernetes with readiness checks and rolling updates. Implement graceful shutdown in the app so connections drain cleanly.

Kubernetes tip: use readiness probes and terminationGracePeriodSeconds.


52 — Detecting and logging slow requests

Explanation: Middleware can measure request duration and log or export a metric when above threshold. Export to Prometheus for alerting.

Code:

app.use((req,res,next) => {
  const start = Date.now();
  res.on('finish', () => {
    const ms = Date.now() - start;
    if (ms > 500) console.warn('slow', req.path, ms);
  });
  next();
});

53 — Feature flags integration in Express

Explanation: Use a feature flag service or config and evaluate flags per request, caching results to reduce latency. Remove stale flags regularly.


54 — Avoiding callback hell in middleware chains

Explanation: Use async/await and wrapper utilities. Keep middleware functions small and composable.

Wrapper pattern: see item 5 wrap.


55 — Request correlation IDs and propagation

Explanation: Generate/read X-Correlation-ID, attach to logs and propagate to downstream services via headers. This enables request-level tracing across services.

Code:

app.use((req,res,next) => {
  req.correlationId = req.headers['x-correlation-id'] || uuid();
  res.setHeader('X-Correlation-ID', req.correlationId);
  next();
});

56 — Integrating job queues (Bull) with Express

Explanation: Push long tasks (emails, image processing) to queues and return a job id to the client. Process jobs in separate worker processes to avoid blocking.

Bull example:

const Queue = require('bull');
const emailQueue = new Queue('emails', { redis: { host:'127.0.0.1' } });
app.post('/send', async (req,res) => {
  const job = await emailQueue.add({ email: req.body.email });
  res.json({ jobId: job.id });
});

57 — API key based authentication pattern

Explanation: Validate x-api-key header against DB or cache; associate metadata (rate limits, owner) with the key.

Simple middleware:

app.use((req,res,next) => {
  const key = req.headers['x-api-key'];
  if (key !== process.env.API_KEY) return res.status(401).end();
  next();
});

58 — JWT verification middleware (practical)

Explanation: Validate token signature and claims (exp, iss, aud). Attach user info to req and handle expired tokens gracefully (use refresh tokens).

Snippet: see item 14.


59 — Combining IP and user-based rate limits

Explanation: Use separate counters keyed by IP and user ID. When authenticated use user counter; otherwise fallback to IP. Use Redis for global counters.


60 — Unit-testing middleware in isolation

Explanation: Mock req, res, and next and assert expected side effects. Use stubs for external dependencies.

Example (sinon style):

const req = { headers: {} };
const res = { status: sinon.stub().returnsThis(), json: sinon.stub() };
const next = sinon.spy();
await myMiddleware(req, res, next);
sinon.assert.calledOnce(next);

61 — Request body transformation middleware

Explanation: Normalize user inputs (trim strings, convert cases) early in middleware so business logic works with canonical forms.

Example:

app.use(express.json());
app.use((req,res,next) => {
  if (req.body.email) req.body.email = req.body.email.toLowerCase().trim();
  next();
});

62 — Avoid blocking the event loop (worker threads)

Explanation: Offload CPU-intensive tasks to worker threads or separate microservices. Keep request handlers non-blocking.

Worker example:

const { Worker } = require('worker_threads');
function runTask(data) {
  return new Promise((resolve, reject) => {
    const w = new Worker('./worker.js', { workerData: data });
    w.on('message', resolve); w.on('error', reject);
  });
}

63 — Middleware composition and reuse

Explanation: Compose arrays of middleware into reusable pipelines for route groups (auth + validation + controller).

Simple compose utility:

const compose = (middleware) => (req,res,next) => {
  const run = i => i < middleware.length ? middleware[i](req,res,() => run(i+1)) : next();
  run(0);
};

64 — Request/response body logging for debugging (careful)

Explanation: Log small request/response bodies in debug mode. Never log sensitive data in production. Use sampling to limit volume.


65 — Dynamic route loading pattern

Explanation: Auto-register route modules from a directory for plugin architectures. Validate module exports.

Example:

fs.readdirSync('./routes').forEach(file => {
  const route = require(`./routes/${file}`);
  app.use(route.prefix, route.router);
});

66 — Hiding internals in production errors

Explanation: Show user-friendly messages (and a requestId) while logging details internally. Toggle full stack traces only in non-production.

Error handler snippet: see item 10.


67 — Optimistic concurrency control pattern

Explanation: Use version columns and WHERE version = ? updates to prevent lost updates, returning conflict (409) if versions mismatch.

SQL example:

UPDATE accounts SET balance = ?, version = version + 1 WHERE id = ? AND version = ?;

68 — A/B testing routing approach

Explanation: Assign users to buckets (cookie or feature service) and use middleware to route to variant implementations. Track metrics per bucket.


69 — Integrating GraphQL with Express

Explanation: Mount Apollo Server or express-graphql on a route. Use dataloaders to avoid N+1 queries and middleware for auth.

Apollo example:

const { ApolloServer } = require('apollo-server-express');
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
server.applyMiddleware({ app, path: '/graphql' });

70 — Prevent filename/path traversal on uploads

Explanation: Generate server-side file names (UUIDs) and disallow user-supplied paths. Use path.join and validate resolved paths.

Example:

const safeName = `${uuid()}${path.extname(req.file.originalname)}`;
fs.writeFileSync(path.join('/uploads', safeName), req.file.buffer);

71 — API pagination patterns (cursor vs offset)

Explanation: Use offset for simple UIs but prefer cursor pagination for performance and consistency when dataset changes. Return opaque cursors.

Cursor response example:

{ "items":[...], "nextCursor":"eyJpZCI6MTIzfQ==" }

72 — Validating content-type (JSON)

Explanation: Enforce Content-Type: application/json for JSON endpoints and return 415 Unsupported Media Type when mismatched.

Code:

if (!req.is('application/json')) return res.status(415).send('Expecting JSON');

73 — Using AbortController for client disconnect handling

Explanation: Listen for req.on('close') and abort downstream operations to save work when the client disconnects.

Example: see item 22.


74 — Streaming response transforms (gzip + JSON stream)

Explanation: Combine stream.Transform with zlib to compress streamed JSON. Use JSONStream to stream large JSON arrays.

Example concept:

readableStream.pipe(JSONStream.stringify()).pipe(zlib.createGzip()).pipe(res);

75 — Multi-tenant data isolation approaches

Explanation: Tenant isolation options: shared DB with tenant column, schema-per-tenant, or DB-per-tenant. Trade-offs: cost vs isolation. Use row-level security for strong tenancy.


76 — Route-specific rate limit (login endpoints)

Explanation: Apply stricter limits to sensitive routes like /login and /password-reset. Use Redis store for distributed enforcement.


77 — HTTP caching headers usage (ETag / Cache-Control)

Explanation: Use ETag and Last-Modified to enable conditional requests and Cache-Control to instruct caches. For dynamic content set private or short max-age.

Example:

res.set('Cache-Control', 'public, max-age=3600');
res.set('ETag', generateEtag(body));

78 — Validating and limiting query parameters

Explanation: Sanitize query values and enforce maximums (e.g., limit <= 100). Reject invalid values.

Example:

const limit = Math.min(parseInt(req.query.limit || 10, 10), 100);

79 — JSON Patch / PATCH semantics (partial updates)

Explanation: Support JSON Patch (application/json-patch+json) or accept partial JSON and apply server-side merge; validate and preserve invariants.

Library: fast-json-patch for applying patches safely.


80 — Transaction management pattern in handlers

Explanation: Start DB transaction in handler, pass transaction object through services, commit on success and rollback on exception.

Example (pseudo):

await db.transaction(async trx => {
  await userRepo.update(trx, ...);
  await orderRepo.create(trx, ...);
});

81 — Limiting concurrent in-flight requests per user

Explanation: Use Redis counters to track concurrent requests by user and reject or queue if limit exceeded to prevent resource exhaustion.


82 — Streaming uploads direct to S3 (pre-signed)

Explanation: Generate pre-signed URLs and let clients upload straight to S3 to offload bandwidth and storage.

Pre-signed URL example: use AWS SDK getSignedUrlPromise('putObject', params).


83 — Correct usage of trust proxy with reverse proxies

Explanation: Only enable trust for known network ranges; otherwise req.ip and req.secure can be spoofed. Use trust proxy carefully.


84 — Normalizing IDs/number formats via middleware

Explanation: Canonicalize IDs or phone numbers on input to simplify downstream logic, but preserve original display formats if needed.


85 — Server-side rendering (SSR) with Express + React

Explanation: Use react-dom/server to render initial HTML then hydrate on the client. SSR improves SEO and first paint but increases server CPU.

Simplified flow:

const html = ReactDOMServer.renderToString(<App />);
res.send(`<html><body><div id="root">${html}</div><script src="/bundle.js"></script></body></html>`);

86 — WebSocket authorization using Express sessions

Explanation: Share session store between Express and WebSocket server; on socket handshake read session cookie and validate.


87 — Idempotency and duplicate request detection

Explanation: Accept Idempotency-Key for POST endpoints and store response/result against that key for a TTL. On duplicate keys return stored result.

Pattern: store (key => response) in Redis.


88 — Request batching approaches (reduce chattiness)

Explanation: Accept array of operations in a single request, validate each, and process with atomicity where required. Provide per-operation results.


89 — Profiling route performance (clinic.js + tracing)

Explanation: Use clinic or Node CPU profilers with traces to identify hot functions. Combine with APM traces for end-to-end latency analysis.


90 — Request-level feature toggles and experiments

Explanation: Use feature flag service with target definitions and deterministic bucketing. Evaluate early in middleware and cache flag choices per user.


91 — Exposing Prometheus metrics safely in Express

Explanation: Expose /metrics only to internal networks or protected endpoints. Use prom-client to register counters/gauges and return promClient.register.metrics().

Example:

const client = require('prom-client');
const counter = new client.Counter({ name: 'http_requests_total', help: '...' });
app.get('/metrics', (req,res) => res.send(client.register.metrics()));

92 — Multipart PATCH semantics (merge vs patch)

Explanation: Choose JSON Merge Patch or JSON Patch and document behavior. Validate patches and ensure transactional application when needed.


93 — Token bucket rate limiting with Redis (burst allowance)

Explanation: Implement token bucket using a Lua script in Redis for atomicity. Allows bursts while enforcing refill rate.


94 — OpenTelemetry instrumentation for Express

Explanation: Use OpenTelemetry SDK and Express instrumentation to create spans and export to collector (Jaeger, Zipkin, or OTLP exporters).


95 — Schema migrations for DBs used by Express apps

Explanation: Use migration tools (Knex, TypeORM) and prefer backward-compatible changes. Deploy migrations in stages for zero-downtime where required.


96 — Dependency security: lockfiles and scanning

Explanation: Use lockfiles, run npm audit and Snyk/Dependabot, and minimize dependencies. Monitor supply chain alerts.


97 — Request validation → transform → controller pipeline (composition)

Explanation: Compose middleware for authentication, validation, and transformation in a predictable order. Keep single-responsibility for each middleware.


98 — Multipart concurrent processing and semaphores

Explanation: Limit concurrency of concurrent multipart file processing (e.g., with p-limit) to avoid memory/IO saturation.


99 — Contract testing Express APIs with Pact

Explanation: Use consumer-driven contracts to guarantee provider/consumer compatibility. Verify contracts in CI before deployments.


100 — Onboarding docs & runbooks for an Express codebase

Explanation: Maintain README, setup scripts, architecture docs, runbooks for common failures, API docs (OpenAPI), and troubleshooting steps. Keep documentation cocreated with code changes.

Checklist example:

  • README.md with dev and prod setup

  • docs/runbook.md: restart steps, log locations, common fixes

  • openapi.yaml or Swagger UI served at /docs