Express Cheatsheet

Building a REST API

Use this Express reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

REST Conventions

Standard resource-oriented endpoint shapes — predictable methods, paths, and status codes.

ActionMethodPathStatus
List allGET/resources200
Get oneGET/resources/:id200 / 404
CreatePOST/resources201 + Location
ReplacePUT/resources/:id200 / 204
Partial updatePATCH/resources/:id200 / 204
DeleteDELETE/resources/:id204 / 200

Minimal REST Router

// routes/users.js
const router = require('express').Router();
const db     = require('../db');

// GET /users
router.get('/', async (req, res, next) => {
  try {
    const users = await db.users.findAll();
    res.json(users);
  } catch (err) { next(err); }
});

// GET /users/:id
router.get('/:id', async (req, res, next) => {
  try {
    const user = await db.users.findById(req.params.id);
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.json(user);
  } catch (err) { next(err); }
});

// POST /users
router.post('/', async (req, res, next) => {
  try {
    const user = await db.users.create(req.body);
    res.status(201)
       .location(`/users/${user.id}`)
       .json(user);
  } catch (err) { next(err); }
});

// PUT /users/:id — full replacement
router.put('/:id', async (req, res, next) => {
  try {
    const user = await db.users.replace(req.params.id, req.body);
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.json(user);
  } catch (err) { next(err); }
});

// PATCH /users/:id — partial update
router.patch('/:id', async (req, res, next) => {
  try {
    const user = await db.users.update(req.params.id, req.body);
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.json(user);
  } catch (err) { next(err); }
});

// DELETE /users/:id
router.delete('/:id', async (req, res, next) => {
  try {
    await db.users.delete(req.params.id);
    res.sendStatus(204);
  } catch (err) { next(err); }
});

module.exports = router;

Mount it:

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

Pagination

router.get('/', async (req, res, next) => {
  try {
    const page  = Math.max(1, parseInt(req.query.page, 10) || 1);
    const limit = Math.min(100, parseInt(req.query.limit, 10) || 20);
    const offset = (page - 1) * limit;

    const [rows, total] = await Promise.all([
      db.items.findAll({ limit, offset }),
      db.items.count(),
    ]);

    const totalPages = Math.ceil(total / limit);

    res.json({
      data: rows,
      meta: { page, limit, total, totalPages },
      links: {
        self:  `/items?page=${page}&limit=${limit}`,
        next:  page < totalPages ? `/items?page=${page + 1}&limit=${limit}` : null,
        prev:  page > 1          ? `/items?page=${page - 1}&limit=${limit}` : null,
        first: `/items?page=1&limit=${limit}`,
        last:  `/items?page=${totalPages}&limit=${limit}`,
      },
    });
  } catch (err) { next(err); }
});

Filtering and Sorting

router.get('/', async (req, res, next) => {
  const SORTABLE  = ['name', 'created_at', 'price'];
  const ORDERABLE = ['asc', 'desc'];

  const sort  = SORTABLE.includes(req.query.sort)   ? req.query.sort   : 'created_at';
  const order = ORDERABLE.includes(req.query.order) ? req.query.order  : 'desc';
  const search = req.query.search?.trim().slice(0, 100) || '';

  try {
    const items = await db.items.search({ sort, order, search });
    res.json(items);
  } catch (err) { next(err); }
});

Consistent Error Envelope

// Centralized error response helper
const fail = (res, status, code, message, details = undefined) =>
  res.status(status).json({ error: { code, message, ...(details && { details }) } });

// Usage
if (!user) return fail(res, 404, 'USER_NOT_FOUND', 'No user with that ID');
if (!valid) return fail(res, 400, 'VALIDATION_ERROR', 'Invalid input', errors);

Input Validation with express-validator

const { body, param, validationResult } = require('express-validator');

const createUserRules = [
  body('email').isEmail().normalizeEmail(),
  body('name').trim().isLength({ min: 1, max: 100 }),
  body('age').optional().isInt({ min: 0, max: 150 }).toInt(),
];

const validate = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  next();
};

router.post('/', createUserRules, validate, async (req, res, next) => {
  // req.body is clean and validated
});

Versioning

// URI versioning (most common)
app.use('/api/v1', require('./routes/v1'));
app.use('/api/v2', require('./routes/v2'));

// Header versioning
app.use('/api', (req, res, next) => {
  const version = req.get('API-Version') || '1';
  req.apiVersion = version;
  next();
});

Nested Resources

// GET /users/:userId/posts
// POST /users/:userId/posts
const postsRouter = require('express').Router({ mergeParams: true });

postsRouter.get('/', async (req, res, next) => {
  // req.params.userId available because mergeParams: true
  const posts = await db.posts.findByUser(req.params.userId);
  res.json(posts);
});

postsRouter.post('/', async (req, res, next) => {
  const post = await db.posts.create({ ...req.body, userId: req.params.userId });
  res.status(201).json(post);
});

// Mount under users router
usersRouter.use('/:userId/posts', postsRouter);

HTTP Status Code Reference

StatusMeaningWhen to Use
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST
204No ContentSuccessful DELETE; PUT/PATCH with no body
400Bad RequestInvalid input, failed validation
401UnauthorizedMissing or invalid credentials
403ForbiddenAuthenticated but not authorized
404Not FoundResource doesn't exist
405Method Not AllowedHTTP method not supported
409ConflictDuplicate, version conflict
410GoneResource permanently deleted
413Payload Too LargeBody exceeds size limit
422Unprocessable EntityValid JSON but semantic errors
429Too Many RequestsRate limited
500Internal Server ErrorUnexpected server failure
503Service UnavailableServer overloaded or down for maintenance

Content Negotiation

router.get('/:id', async (req, res, next) => {
  try {
    const resource = await db.items.findById(req.params.id);
    if (!resource) return res.sendStatus(404);

    res.format({
      'application/json': () => res.json(resource),
      'text/html':        () => res.render('item', { resource }),
      'text/csv':         () => res.send(toCsv(resource)),
      default:            () => res.status(406).send('Not Acceptable'),
    });
  } catch (err) { next(err); }
});