All topics
Backend · Learning hub

Express notes for developers

Master Express with a curated set of 7 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Backend notes
Express

Middleware & Request Pipeline

Express Middleware & Request Pipeline Middleware functions have access to request, response objects and the next middleware function. They execute in order and

Express Middleware & Request Pipeline

Middleware functions have access to request, response objects and the next middleware function. They execute in order and can modify req/res or end the request.

Basic Middleware

const express = require('express');
const app = express();

// Application-level middleware
app.use((req, res, next) => {
  console.log('Time:', Date.now());
  next();  // Pass to next middleware
});

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

// Parse JSON body
app.use(express.json());

// Parse URL-encoded body
app.use(express.urlencoded({ extended: true }));

// Static files
app.use(express.static('public'));

// Route-specific middleware
app.get('/api/users', authMiddleware, (req, res) => {
  res.json(users);
});

// Error-handling middleware (4 parameters)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal Server Error' });
});
Express

Routing & Route Handlers

Express Routing const express = require('express'); const app = express(); // Basic routes app.get('/', (req, res) => { res.send('Hello World'); }); app.post('/

Express Routing

const express = require('express');
const app = express();

// Basic routes
app.get('/', (req, res) => {
  res.send('Hello World');
});

app.post('/api/users', (req, res) => {
  const user = req.body;
  res.status(201).json(user);
});

// Route parameters
app.get('/users/:id', (req, res) => {
  const { id } = req.params;
  res.json({ id, name: 'User' });
});

// Query parameters
app.get('/search', (req, res) => {
  const { q, page = 1 } = req.query;
  res.json({ query: q, page });
});

// Router modules
const userRouter = express.Router();

userRouter.get('/', (req, res) => {
  res.json(users);
});

userRouter.get('/:id', (req, res) => {
  res.json(user);
});

userRouter.post('/', (req, res) => {
  res.status(201).json(newUser);
});

app.use('/api/users', userRouter);
Express

Interview Questions

Express Interview Questions Common Express.js questions in backend and Node.js interviews. These range from framework fundamentals to architecture decisions and

Express Interview Questions

Common Express.js questions in backend and Node.js interviews. These range from framework fundamentals to architecture decisions and security - topics that come up in both junior and senior developer interviews.

1. What is Express.js and why would you use it over plain Node.js?

Express is a minimal, unopinionated web framework built on top of Node.js's http module. While you can build an HTTP server in Node.js directly, Express adds conveniences like routing (app.get, app.post etc.), middleware composition, request/response augmentation (req.params, res.json etc.), and a large ecosystem of compatible middleware packages. It handles the repetitive boilerplate of HTTP so you can focus on application logic. "Unopinionated" means Express does not force any particular project structure or tools - you choose what to combine it with.

2. What is middleware in Express and how does it work?

Middleware is a function with the signature (req, res, next). Express passes each request through a stack of middleware functions in the order they are registered with app.use() or route handlers. Each middleware can read/modify req and res, end the response cycle (e.g., res.json()), or call next() to pass control to the next middleware. If a middleware does not call next() and does not send a response, the request hangs. Calling next(err) skips to the nearest error-handling middleware (which has 4 parameters: err, req, res, next).

3. What is the difference between app.use() and app.get()?

app.use() registers middleware that matches all HTTP methods (GET, POST, PUT, DELETE, etc.) and performs prefix matching on the path (app.use('/api') matches /api, /api/users, /api/posts, etc.). app.get() registers a handler for GET requests only and performs exact path matching (plus route params). app.use() is typically used for middleware and mounting routers; app.get() and siblings are used for specific route handlers.

4. How do you handle errors in Express?

Express has a dedicated error-handling mechanism: a middleware with four parameters (err, req, res, next). To trigger it, call next(err) with an error object from any route or middleware. You should have one global error handler registered after all routes. For async route handlers, you must catch errors and pass them to next() - either with try/catch or by wrapping handlers in an asyncHandler utility. Common pattern: create a custom AppError class with statusCode and code properties to distinguish operational errors (expected, safe to expose) from programming errors (unexpected, should not reveal details).

5. What is express.Router() and why use it?

express.Router() creates a mini Express application - a modular, mountable route handler. Instead of defining all routes on the main app object, you define related routes on a Router instance and mount it at a path prefix: app.use('/api/users', usersRouter). This keeps code organized, enables route-level middleware that only applies to that router, and makes it easy to split routes into separate files as the API grows. Routers can also be nested.

6. How would you implement authentication in Express?

The most common approach for REST APIs is JWT (JSON Web Token) authentication. On login, the server verifies credentials, signs a JWT with a secret key containing user claims (id, role), and returns it to the client. The client stores the token and includes it in the Authorization: Bearer <token> header on subsequent requests. An authenticate middleware verifies the token on every protected route: it extracts the token from the header, verifies it with jwt.verify(), and attaches the decoded payload to req.user. Passwords must be hashed with bcrypt before storing. Sessions with express-session and a session store (Redis) are the alternative for traditional web apps.

7. What security measures should every Express API have?

Essential security layers: (1) helmet - sets security HTTP headers (prevents XSS, clickjacking, MIME sniffing). (2) CORS configuration - restrict which origins can call your API. (3) Rate limiting with express-rate-limit - prevent brute force and DDoS. (4) Input validation - validate and sanitize all incoming data (Zod, express-validator). (5) Request size limits - express.json({ limit: '10kb' }) prevents payload attacks. (6) Parameterized queries - prevent SQL injection. (7) Hash passwords with bcrypt. (8) HTTPS in production. (9) Keep dependencies updated and audit with npm audit.

8. How do you structure a large Express application?

Separate concerns into layers: (1) Routes - define endpoints and attach middleware. (2) Controllers - handle HTTP req/res, call services, return responses. (3) Services - business logic, orchestration, should not know about HTTP. (4) Repositories/Models - data access layer. (5) Middleware - auth, validation, logging, error handling. (6) Config - environment variables, database connections. Keep index.ts just for starting the server; create app.ts to configure the Express instance so it can be imported in tests without starting a server. Use dependency injection or a service locator for testability.

9. How do you handle async/await in Express route handlers?

Express does not handle promise rejections automatically in route handlers. If an async route throws or rejects, you must catch the error and pass it to next(err). Two approaches: (1) Explicit try/catch in every handler - verbose but clear. (2) Wrap handlers with an asyncHandler utility: const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next). Then use it: app.get('/users', asyncHandler(async (req, res) => { ... })). Express 5 (currently in beta) handles async errors automatically without the wrapper.

10. What is the order of middleware and why does it matter?

Express middleware executes in the exact order it is registered. This has important consequences: (1) body-parser/express.json() must come before routes that read req.body. (2) Authentication middleware must come before route handlers that require auth. (3) The 404 handler must come after all routes (otherwise valid routes would be caught by it). (4) The global error handler must come last. A good order is: security middleware (helmet, cors) -> request parsing (json, urlencoded) -> logging -> rate limiting -> routes -> 404 handler -> error handler.

Express

Setup & Basics

Express.js Setup & Basics Express is a minimal, unopinionated web framework for Node.js. It provides a thin layer of fundamental web application features - rout

Express.js Setup & Basics

Express is a minimal, unopinionated web framework for Node.js. It provides a thin layer of fundamental web application features - routing, middleware, and HTTP utilities - without obscuring the Node.js features you already know.

Installation & First Server

# Initialize project
npm init -y
npm install express
npm install -D @types/express typescript ts-node nodemon  # TypeScript setup

# nodemon for auto-restart in development
npx nodemon src/index.ts
// src/index.js - Basic Express server
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Built-in middleware
app.use(express.json());                         // Parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies
app.use(express.static('public'));               // Serve static files

// Basic route
app.get('/', (req, res) => {
  res.send('Hello, World!');
});

// Start server
app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
});

TypeScript Setup

// src/index.ts - Express with TypeScript
import express, { Request, Response, NextFunction } from 'express';

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

app.get('/', (req: Request, res: Response) => {
  res.json({ message: 'Hello, World!', timestamp: new Date().toISOString() });
});

app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
});

export default app;

Request & Response Objects

Express extends Node's http.IncomingMessage and http.ServerResponse with convenient properties and methods. Understanding these is fundamental to working with Express.

app.post('/api/users/:id/posts', (req, res) => {
  // Request properties
  console.log(req.params);      // { id: '42' } - URL params
  console.log(req.query);       // { page: '1', limit: '10' } - query string
  console.log(req.body);        // { title: 'Hello' } - parsed request body
  console.log(req.headers);     // Request headers
  console.log(req.method);      // 'POST'
  console.log(req.path);        // '/api/users/42/posts'
  console.log(req.url);         // Full URL with query string
  console.log(req.ip);          // Client IP address
  console.log(req.cookies);     // Parsed cookies (requires cookie-parser)

  // Response methods
  res.status(201)               // Set status code (chainable)
    .set('X-Custom-Header', 'value')  // Set response header
    .json({ id: 1, title: 'Hello' }); // Send JSON (sets Content-Type)

  // Other response methods:
  // res.send('text')           - Send string/Buffer/object
  // res.sendFile('/path')      - Send a file
  // res.redirect('/new-url')   - 302 redirect
  // res.redirect(301, '/new')  - Permanent redirect
  // res.render('view', data)   - Render a template
  // res.end()                  - End response with no body
  // res.download('/path')      - Prompt file download
});

Project Structure

A scalable Express project separates concerns into layers: routes define endpoints, controllers handle request/response logic, services contain business logic, and models/repositories handle data access.

src/
  index.ts           # Server entry point
  app.ts             # Express app setup (routes, middleware)
  routes/
    users.ts         # /api/users routes
    auth.ts          # /api/auth routes
  controllers/
    usersController.ts
    authController.ts
  services/
    usersService.ts
    emailService.ts
  middleware/
    auth.ts          # JWT verification
    validate.ts      # Request validation
    errorHandler.ts  # Global error handler
  models/
    User.ts
  config/
    database.ts
    env.ts
Express

Authentication & Security

Express Authentication & Security Security is not optional. Every production Express API needs authentication, input validation, rate limiting, and proper secur

Express Authentication & Security

Security is not optional. Every production Express API needs authentication, input validation, rate limiting, and proper security headers. This page covers the essential patterns and packages for building secure Express applications.

JWT Authentication

JSON Web Tokens (JWT) are a stateless authentication mechanism. The server signs a token containing user claims; the client sends it with every request. No session store required.

npm install jsonwebtoken bcryptjs
npm install -D @types/jsonwebtoken @types/bcryptjs
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const JWT_SECRET = process.env.JWT_SECRET; // Must be a long random string
const JWT_EXPIRES_IN = '7d';

// Register endpoint
app.post('/api/auth/register', async (req, res, next) => {
  try {
    const { email, password, name } = req.body;
    const hashedPassword = await bcrypt.hash(password, 12);
    const user = await User.create({ email, password: hashedPassword, name });
    const token = jwt.sign(
      { userId: user.id, email: user.email, role: user.role },
      JWT_SECRET,
      { expiresIn: JWT_EXPIRES_IN }
    );
    res.status(201).json({ token, user: { id: user.id, email, name } });
  } catch (err) { next(err); }
});

// Login endpoint
app.post('/api/auth/login', async (req, res, next) => {
  try {
    const { email, password } = req.body;
    const user = await User.findByEmail(email);
    if (!user || !(await bcrypt.compare(password, user.password))) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    const token = jwt.sign(
      { userId: user.id, email: user.email, role: user.role },
      JWT_SECRET,
      { expiresIn: JWT_EXPIRES_IN }
    );
    res.json({ token });
  } catch (err) { next(err); }
});

// JWT middleware
const authenticate = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }
  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
};

// Role-based authorization
const authorize = (...roles) => (req, res, next) => {
  if (!roles.includes(req.user.role)) {
    return res.status(403).json({ error: 'Insufficient permissions' });
  }
  next();
};

app.get('/api/admin/stats', authenticate, authorize('admin'), (req, res) => {
  res.json({ stats: 'admin only data' });
});

Input Validation

Never trust client input. Validate and sanitize every request body, query parameter, and route param. Zod and express-validator are the most popular choices.

// Using Zod for validation
const { z } = require('zod');

const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/),
  age: z.number().int().min(18).max(120).optional(),
});

// Validation middleware factory
const validate = (schema) => (req, res, next) => {
  const result = schema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({
      error: 'Validation failed',
      details: result.error.errors.map(e => ({
        field: e.path.join('.'),
        message: e.message,
      })),
    });
  }
  req.validated = result.data;  // Attach validated + typed data
  next();
};

app.post('/api/users', validate(createUserSchema), async (req, res, next) => {
  try {
    const user = await User.create(req.validated);
    res.status(201).json(user);
  } catch (err) { next(err); }
});

// Query param validation
const listUsersSchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  search: z.string().optional(),
});

const validateQuery = (schema) => (req, res, next) => {
  const result = schema.safeParse(req.query);
  if (!result.success) return res.status(400).json({ error: 'Invalid query params' });
  req.query = result.data;
  next();
};

Rate Limiting & Security Headers

const rateLimit = require('express-rate-limit');
const helmet = require('helmet');

// Security headers (XSS, clickjacking, MIME sniffing, etc.)
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", 'https://fonts.googleapis.com'],
      imgSrc: ["'self'", 'data:', 'https:'],
    },
  },
}));

// General API rate limit
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 min window
  max: 100,
  message: { error: 'Too many requests, retry after 15 minutes' },
});
app.use('/api/', apiLimiter);

// Stricter limit for auth endpoints (prevent brute force)
const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour window
  max: 10,                   // 10 attempts per hour
  skipSuccessfulRequests: true,  // Only count failures
  message: { error: 'Too many failed attempts, try again in an hour' },
});
app.use('/api/auth/', authLimiter);

// Prevent large payload attacks
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));

// HTTPS redirect in production
app.use((req, res, next) => {
  if (process.env.NODE_ENV === 'production' && !req.secure) {
    return res.redirect(301, 'https://' + req.headers.host + req.url);
  }
  next();
});
Express

Middleware

Express Middleware Middleware functions are the backbone of Express. They have access to the request and response objects and the next() function. Middleware ca

Express Middleware

Middleware functions are the backbone of Express. They have access to the request and response objects and the next() function. Middleware can execute code, modify req/res, end the request-response cycle, or call the next middleware in the stack.

Writing Custom Middleware

// Application-level middleware (runs on every request)
app.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
  next(); // MUST call next() or the request hangs
});

// Middleware factory - returns middleware with custom options
function requestLogger(options = {}) {
  const { prefix = 'LOG' } = options;
  return (req, res, next) => {
    console.log(`[${prefix}] ${req.method} ${req.url}`);
    next();
  };
}
app.use(requestLogger({ prefix: 'API' }));

// Middleware that attaches data to req for downstream handlers
app.use(async (req, res, next) => {
  try {
    req.requestId = crypto.randomUUID();
    req.startTime = Date.now();
    next();
  } catch (err) {
    next(err); // Pass errors to error-handling middleware
  }
});

// Route-specific middleware
const requireAdmin = (req, res, next) => {
  if (req.user?.role !== 'admin') {
    return res.status(403).json({ error: 'Admin access required' });
  }
  next();
};

app.get('/admin/users', requireAdmin, (req, res) => {
  res.json(allUsers);
});

Error-Handling Middleware

Error-handling middleware has four parameters: (err, req, res, next). It must be defined AFTER all routes and regular middleware. Errors are passed to it by calling next(err) anywhere in the pipeline.

// Custom error class
class AppError extends Error {
  constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.isOperational = true;
  }
}

// 404 handler (place after all routes)
app.use((req, res, next) => {
  next(new AppError(`Route ${req.path} not found`, 404, 'NOT_FOUND'));
});

// Global error handler (4 params - Express identifies this as error handler)
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const message = err.isOperational ? err.message : 'Something went wrong';

  // Log unexpected errors
  if (!err.isOperational) {
    console.error('Unexpected error:', err);
  }

  res.status(statusCode).json({
    error: {
      message,
      code: err.code || 'INTERNAL_ERROR',
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
    },
  });
});

// Async error wrapper (avoids try-catch in every route)
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await db.findUser(req.params.id);
  if (!user) throw new AppError('User not found', 404);
  res.json(user);
}));

Third-Party Middleware

The Express ecosystem has well-maintained middleware for every common need. These are the packages you will reach for in almost every production application.

npm install cors helmet morgan cookie-parser compression express-rate-limit
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const compression = require('compression');
const rateLimit = require('express-rate-limit');

// Security headers
app.use(helmet());

// CORS configuration
app.use(cors({
  origin: ['https://myapp.com', 'https://www.myapp.com'],
  credentials: true,         // Allow cookies/auth headers
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
}));

// HTTP request logging
app.use(morgan('combined')); // 'dev' for development, 'combined' for production

// Cookie parsing
app.use(cookieParser(process.env.COOKIE_SECRET));
// Access cookies: req.cookies.name, req.signedCookies.name

// Gzip compression for responses
app.use(compression());

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                   // Max 100 requests per window per IP
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many requests, please try again later.' },
});
app.use('/api/', limiter);
Express

Routing

Express Routing Routing determines how an application responds to client requests at specific endpoints. Express provides a full-featured routing system that su

Express Routing

Routing determines how an application responds to client requests at specific endpoints. Express provides a full-featured routing system that supports route parameters, query strings, route-level middleware, and modular Router instances for organizing large APIs.

Basic Routing & HTTP Methods

// HTTP method handlers
app.get('/api/users', (req, res) => { /* ... */ });
app.post('/api/users', (req, res) => { /* ... */ });
app.put('/api/users/:id', (req, res) => { /* ... */ });
app.patch('/api/users/:id', (req, res) => { /* ... */ });
app.delete('/api/users/:id', (req, res) => { /* ... */ });
app.options('/api/users', (req, res) => { /* ... */ });

// Handle all HTTP methods
app.all('/api/users', (req, res) => {
  res.json({ method: req.method });
});

// Chained route handlers for the same path
app.route('/api/users/:id')
  .get((req, res) => {
    res.json({ id: req.params.id });
  })
  .put((req, res) => {
    res.json({ updated: req.params.id });
  })
  .delete((req, res) => {
    res.status(204).end();
  });

// Multiple handlers per route (middleware chain)
app.get('/api/data', authenticate, authorize('read'), (req, res) => {
  res.json({ data: 'secret' });
});

Route Parameters & Query Strings

// Route parameters - :param in path
app.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params;
  // GET /users/42/posts/7 => { userId: '42', postId: '7' }
  res.json({ userId, postId });
});

// Optional parameter
app.get('/users/:userId/profile/:section?', (req, res) => {
  const section = req.params.section || 'overview';
  res.json({ section });
});

// Query strings
app.get('/api/products', (req, res) => {
  const {
    search = '',
    category,
    page = '1',
    limit = '20',
    sortBy = 'name',
    order = 'asc',
  } = req.query;
  // GET /api/products?search=laptop&category=tech&page=2
  const pageNum = parseInt(page, 10);
  const limitNum = parseInt(limit, 10);
  const offset = (pageNum - 1) * limitNum;

  res.json({ search, category, pageNum, limitNum, offset, sortBy, order });
});

// Route parameter middleware (runs before any route with :userId)
app.param('userId', async (req, res, next, id) => {
  try {
    const user = await User.findById(id);
    if (!user) return res.status(404).json({ error: 'User not found' });
    req.user = user;  // Attach to req for route handlers
    next();
  } catch (err) {
    next(err);
  }
});

Express Router - Modular Routes

Use express.Router() to create modular route handlers. Each router is a mini Express application with its own middleware and routing. Mount routers at a path prefix in your main app.

// routes/users.js
const express = require('express');
const router = express.Router();
const { authenticate } = require('../middleware/auth');
const usersController = require('../controllers/usersController');

// Router-level middleware (applies to all routes in this file)
router.use(authenticate);

// Routes (paths are relative to the mount point)
router.get('/', usersController.list);           // GET /api/users
router.post('/', usersController.create);        // POST /api/users
router.get('/:id', usersController.getById);     // GET /api/users/:id
router.put('/:id', usersController.update);      // PUT /api/users/:id
router.delete('/:id', usersController.remove);   // DELETE /api/users/:id

module.exports = router;

// app.js - mount the router
const usersRouter = require('./routes/users');
app.use('/api/users', usersRouter);

// Nest routers
const v1Router = express.Router();
v1Router.use('/users', usersRouter);
v1Router.use('/products', productsRouter);
app.use('/api/v1', v1Router);

RESTful API Pattern

// controllers/usersController.js - RESTful controller
const User = require('../models/User');

exports.list = async (req, res, next) => {
  try {
    const users = await User.findAll();
    res.json({ data: users, total: users.length });
  } catch (err) { next(err); }
};

exports.create = async (req, res, next) => {
  try {
    const user = await User.create(req.body);
    res.status(201).json({ data: user });
  } catch (err) { next(err); }
};

exports.getById = async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.json({ data: user });
  } catch (err) { next(err); }
};

exports.update = async (req, res, next) => {
  try {
    const user = await User.findByIdAndUpdate(req.params.id, req.body);
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.json({ data: user });
  } catch (err) { next(err); }
};

exports.remove = async (req, res, next) => {
  try {
    await User.findByIdAndDelete(req.params.id);
    res.status(204).end();
  } catch (err) { next(err); }
};

Keep your Express knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever