Flask Pocket Book

Express.js Pocket Book — Uplatz

50 deep-dive flashcards • Wide layout • Fewer scrolls • 20+ Interview Q&A • Readable code examples

Section 1 — Fundamentals

1) What is Express.js?

Express is a minimalist web framework for Node.js that provides routing, middleware, and HTTP utilities with very little abstraction. It lets you compose web servers and APIs quickly while staying close to the Node platform. Great for REST APIs, server‑rendered apps, and backends for SPAs.

npm init -y
npm i express

2) Why Express? Strengths & Tradeoffs

Strengths: tiny core, huge ecosystem, familiar patterns, easy onboarding, works with any data layer or view engine. Tradeoffs: fewer batteries included (validation, structure, types), and it’s easy to build inconsistent code without conventions. Add linters, a router structure, schemas, and tests.

# Quick start file
node src/server.js

3) Hello World (Minimal)

The smallest server: one route, one response. Use environment variables for the port and set sensible defaults.

import express from 'express';
const app = express();
app.get('/health', (_req, res) => res.json({ ok: true }));
app.listen(process.env.PORT || 3000);

Tip: prefer /health and /ready endpoints for probes.

4) Folder Layout & Scripts

Keep code modular. One common layout: src/app.ts (build app), src/server.ts (start), src/routes, src/middleware, src/controllers, src/lib.

"scripts": {
  "dev": "node --watch src/server.js",
  "test": "node --test",
  "start": "NODE_ENV=production node dist/server.js"
}

5) Request & Response Basics

Handlers receive (req, res, next). req exposes params, query, body, headers; res provides helpers like res.json(), res.status(), res.cookie(), res.sendFile().

app.post('/echo', (req, res) => {
  res.status(201).json({ youSent: req.body });
});

6) Middleware Concept

Middleware are functions that run in sequence to read/modify req/res or short‑circuit the pipeline. Order matters. Use global middleware for cross‑cutting concerns (parsing, CORS, security headers) and route‑level for features.

app.use(express.json());
app.use((req, _res, next) => { req.start = Date.now(); next(); });

7) Built‑in Middleware

Express ships with JSON/urlencoded parsers and static serving. Tune limits and trust proxy appropriately.

app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: true }));
app.use('/public', express.static('public'));

8) Routers & Modularization

Use express.Router() to split large apps. Each router gets its own middleware and routes; mount with a base path.

import { Router } from 'express';
const users = Router();
users.get('/', listUsers);
users.post('/', createUser);
app.use('/users', users);

9) Env & Configuration

Load configuration at startup, validate, and fail fast if required values are missing.

import 'dotenv/config';
const cfg = { dbUrl: process.env.DB_URL };
if (!cfg.dbUrl) throw new Error('DB_URL required');

10) Q&A — “Is Express still a good choice in 2025?”

Answer: Yes. Express remains stable, widely understood, and ecosystem‑rich. If you want stronger schemas/perf by default, pick Fastify/Nest. Otherwise, Express + conventions (validation, structure, tests) delivers excellent results with minimal friction.

Section 2 — Routing, Params & Middleware

11) Route Methods & Paths

Define handlers for HTTP verbs and patterns; chain common middleware and group related endpoints.

app.route('/users')
  .get(listUsers)
  .post(validateUser, createUser);
app.get('/users/:id', getUser);

12) Params & Query

Params are part of the path; queries are key/values after ?. Validate both to avoid injection and logic bugs.

app.get('/orders/:id', (req, res) => {
  const { id } = req.params;        // "/orders/123"
  const { include } = req.query;    // "?include=items"
  res.json({ id, include });
});

13) Async Handlers & Errors

Wrap async handlers to forward rejections to the error middleware; avoid unhandled promise rejections.

const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req,res,next)).catch(next);
app.get('/slow', asyncHandler(async (_req, res) => {
  const data = await fetchStuff();
  res.json(data);
}));

14) Router‑level Middleware

Apply middleware to a specific router for auth, rate limits, or parsing.

const admin = Router();
admin.use(requireAdmin);
admin.get('/stats', stats);
app.use('/admin', admin);

15) Error‑Handling Middleware

The signature (err, req, res, next) marks an error handler. Centralize error shapes and logging.

app.use((err, _req, res, _next) => {
  const code = err.status || 500;
  res.status(code).json({ error: err.message, code });
});

16) Input Validation

Validate req.body, req.params, and req.query with zod/joi/celebrate to prevent malformed requests and implicit coercions.

import { z } from 'zod';
const schema = z.object({ email: z.string().email() });
app.post('/signup', (req, res, next) => {
  const parsed = schema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json(parsed.error);
  next();
}, signup);

17) File Uploads

Use multer for multipart forms; stream to disk/cloud. Enforce size/type limits and scan untrusted files.

import multer from 'multer';
const upload = multer({ dest: 'uploads/' });
app.post('/avatar', upload.single('file'), handleAvatar);

18) CORS

Configure allowed origins, headers, and credentials; don’t open wildcard * on private APIs.

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

19) Security Headers

Helmet sets sane defaults (CSP, HSTS, XSS protection). Review CSP for your assets and 3rd‑party scripts.

import helmet from 'helmet';
app.use(helmet());

20) Q&A — “Middleware order vs routes?”

Answer: Order is top‑down. App‑level middleware runs before mounted routers; router middleware runs before its routes. Error handlers at the end catch thrown/forwarded errors. Always register body parsers, CORS, and security headers before your routes.

Section 3 — Data, Views, Sessions & Auth

21) View Engines (SSR)

Express supports Pug, EJS, Handlebars, etc. Useful for server‑rendered pages and emails. Cache templates in prod.

import path from 'node:path';
app.set('view engine', 'pug');
app.set('views', path.join(process.cwd(), 'views'));
app.get('/', (_req, res) => res.render('home', { title: 'Welcome' }));

22) PostgreSQL with Prisma

Type‑safe DB access and migrations. Use connection pools and handle shutdown cleanly.

import { PrismaClient } from '@prisma/client';
const db = new PrismaClient();
app.get('/users', async (_req, res) => {
  res.json(await db.user.findMany());
});

23) MongoDB with Mongoose

Mongoose adds schemas, validation, hooks. Keep documents bounded and index hot queries.

import mongoose from 'mongoose';
await mongoose.connect(process.env.MONGO_URL);
const User = mongoose.model('User', new mongoose.Schema({ email: String }));

24) Data Access Layer

Encapsulate DB logic in services/repositories to keep controllers thin and testable.

// user.service.js
export async function create(data){ return db.user.create({ data }); }

25) Cookies & Sessions

Use cookie‑based sessions for web apps; keep secrets strong; set httpOnly, secure, and sameSite correctly.

import session from 'express-session';
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false, saveUninitialized: false,
  cookie: { httpOnly: true, secure: true, sameSite: 'lax' }
}));

26) JWT Auth (Stateless APIs)

Issue short‑lived access tokens and rotate refresh tokens. Validate algorithms and audiences.

import jwt from 'jsonwebtoken';
function auth(req, res, next){
  const token = req.headers.authorization?.split(' ')[1];
  try { req.user = jwt.verify(token, process.env.PUBLIC_KEY); next(); }
  catch(e){ return res.status(401).json({ error: 'invalid token' }); }
}

27) Passport / OIDC

For social logins or enterprise SSO, use Passport strategies or dedicated OIDC libraries. Store only necessary claims.

import passport from 'passport';
app.use(passport.initialize());
app.get('/auth/google', passport.authenticate('google', { scope:['email','profile'] }));

28) Authorization (RBAC/ABAC)

Check permissions after authentication. Keep checks near business logic and expose consistent error codes.

function requireRole(role){
  return (req, res, next) => req.user?.roles?.includes(role)
    ? next() : res.status(403).json({ error:'forbidden' });
}

29) Rate Limits & Abuse Control

Use Redis‑backed counters to enforce per‑IP/user limits and return 429 with Retry‑After.

import rateLimit from 'express-rate-limit';
app.use('/api/', rateLimit({ windowMs: 60_000, max: 120 }));

30) Q&A — “JWT vs Sessions?”

Answer: Use sessions (cookies + server store) for browser apps needing logout and rotation on privilege change. Use JWT for stateless APIs/mobile, but keep TTL short and protect refresh flows. Both require HTTPS and CSRF/XSS mitigations.

Section 4 — Performance, Streaming, Scaling & Patterns

31) Performance Tips

Avoid sync APIs, enable gzip/br, reuse connections, and paginate heavy endpoints. Cache expensive reads and use ETags.

import compression from 'compression';
app.use(compression());
app.set('etag', 'strong');

32) Proxies & trust proxy

Behind Nginx/Heroku, set app.set(‘trust proxy’, 1) to honor X‑Forwarded‑For/Proto. Required for correct IPs and secure cookies.

app.set('trust proxy', 1);

33) Clustering

Use Node cluster/PM2 to utilize multiple CPU cores. Combine with a reverse proxy for zero‑downtime restarts.

import cluster from 'node:cluster';
import os from 'node:os';
if (cluster.isPrimary) os.cpus().forEach(() => cluster.fork());
else app.listen(3000);

34) Streaming & SSE

Stream large responses and use Server‑Sent Events for one‑way real‑time updates.

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

35) WebSockets with Express

Pair Express with Socket.IO or ws for bidirectional comms. Share auth/session context carefully.

import { createServer } from 'node:http';
import { Server } from 'socket.io';
const http = createServer(app);
const io = new Server(http);
io.on('connection', (s) => s.emit('hello','world'));

36) File Downloads & Ranges

Support range requests for media and set appropriate cache headers.

app.get('/files/:id', (req, res) => {
  // validate & stream from storage
  res.setHeader('Cache-Control','private, max-age=60');
});

37) Central Error & Problem Details

Return consistent error shapes (RFC7807 style) for better DX; include type, title, status, detail.

res.status(404).json({
  type: 'https://http.dev/not-found', title: 'Not Found', status: 404,
  detail: 'User not found'
});

38) Logging

Use structured logs with request IDs for correlation; avoid console.log in production.

import pino from 'pino';
import pinoHttp from 'pino-http';
const log = pino();
app.use(pinoHttp({ logger: log }));

39) API Versioning

Version via pathname (/v1) or header (Accept: application/vnd.app.v1+json). Deprecate with headers and timelines.

res.setHeader('Deprecation','true');
res.setHeader('Sunset','Wed, 31 Dec 2025 23:59:59 GMT');

40) Q&A — “Keep Express responsive under load?”

Answer: Avoid CPU work on the main thread, set timeouts, stream big payloads, cache hot reads, rate‑limit abusive traffic, and move long tasks to queues/workers. Scale horizontally with cluster/containers and put a CDN/proxy in front.

Section 5 — Testing, CI/CD, Deployment, Observability & Interview Q&A

41) HTTP Testing

Use supertest with Node’s test runner/Jest. Export the app separately from the server to test without listening on a port.

import request from 'supertest';
import { app } from '../src/app.js';
import test from 'node:test';
import assert from 'node:assert/strict';

test('GET /health', async () => {
  const res = await request(app).get('/health');
  assert.equal(res.status, 200);
});

42) Contract & Schema Tests

Validate inputs/outputs against OpenAPI/JSON Schema to prevent drift between services and docs.

// example: zod schema parse in tests
expect(() => schema.parse(res.body)).not.toThrow();

43) CI Pipeline

Lint, type‑check, test, and build in CI; fail fast on vulnerabilities.

- run: npm ci
- run: npm run lint
- run: npm test -- --coverage
- run: npm run build

44) Docker Image

Create small, reproducible images with pinned Node versions and non‑root users.

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node","dist/server.js"]

45) Secrets & Config

Read secrets from your platform (AWS SSM, GCP Secret Manager). Never bake secrets into images; rotate regularly.

// load from env at startup & validate
if (!process.env.JWT_PRIVATE_KEY) process.exit(1);

46) Observability

Add metrics and distributed tracing. Track latency distributions (p50/p95/p99) and error rates per route.

import client from 'prom-client';
const httpReqs = new client.Counter({ name:'http_requests_total', help:'requests', labelNames:['route','code'] });

47) Production Checklist

  • HTTPS only; HSTS; security headers
  • Input validation everywhere
  • Request timeouts & body size limits
  • Central error handler & structured logs
  • Health/readiness endpoints; graceful shutdown
  • Rate limits, authz audits, and runbooks

48) Common Pitfalls

Missing body parsers, mixing async errors with sync handlers, trusting client‑supplied IDs without authz checks, not setting trust proxy, forgetting timeouts, and inconsistent error shapes.

49) Example App Skeleton

A minimal, test‑friendly split between app and server.

// src/app.js
import express from 'express';
export const app = express();
app.use(express.json());
app.get('/health', (_req,res) => res.json({ ok:true }));

// src/server.js
import { app } from './app.js';
app.listen(process.env.PORT || 3000);

50) Interview Q&A — 20 Practical Questions (Express‑focused)

1) Why Express for APIs? Minimal, flexible, huge ecosystem; easy to integrate any DB/view/queue.

2) How do you structure a large Express app? Routers per domain, services for DB access, shared middleware, centralized errors.

3) How do you handle async errors? Wrap handlers and forward with next(err); final error middleware shapes response.

4) How do you enforce input schemas? zod/joi/celebrate; validate body/params/query per route.

5) How to secure cookies? httpOnly, secure, sameSite, rotate secrets; set trust proxy for TLS‑terminating proxies.

6) JWT vs sessions? Sessions for web apps; JWT for stateless APIs. Keep TTL short and rotate.

7) Where to implement authorization? Near business logic (service layer) or dedicated middleware using RBAC/ABAC.

8) How to rate‑limit? Redis‑backed counters (express-rate-limit) per IP/user/route; return 429 + Retry‑After.

9) How to do request logging? pino-http/morgan with request IDs; include route, code, latency.

10) Prevent slowloris/DoS? Body size limits, timeouts, keep‑alive tuning, proxies, and WAF/CDN.

11) Serve static assets safely? From a dedicated CDN; if via Express, use express.static with cache headers.

12) How to implement file uploads? multer with size/type limits; scan files; stream to storage.

13) WebSockets with Express? Use Socket.IO/ws alongside HTTP server; share auth.

14) Graceful shutdown? Stop accepting, drain connections/queues, close DB pools, flush logs, then exit 0.

15) Version your API? Path or header versioning; provide deprecation headers and timelines.

16) Testing strategy? Unit services, integration with supertest, contract/schema tests, selective e2e.

17) Prevent CORS issues? Configure origins, credentials, and headers via cors properly.

18) Why central error shapes? Consistent DX, easier client handling, better observability.

19) Common perf wins? Compression, caching, pagination, batching, avoiding sync FS, pool DB connections.

20) When NOT to pick Express? If you need schema‑first routing and max throughput out of the box (Fastify) or an opinionated architecture (Nest).