Skip to content

Advanced API Routing Patterns

Real-world APIs need more than flat CRUD routes, they need nested resources, filtering, and pagination. This lesson covers those patterns on top of what you've already built.

Nested Resources

When one resource belongs to another, comments belonging to a post, items belonging to an order, nested routes express that relationship directly in the URL: /posts/:postId/comments. Express's express.Router({ mergeParams: true }) lets a nested router still access the parent's route parameters.

Filtering, sorting, and pagination, on the other hand, are usually expressed through query parameters rather than the path itself, since they modify how a collection is retrieved without changing which resource is being addressed.

import express from 'express';

const commentsRouter = express.Router({ mergeParams: true });

commentsRouter.get('/', (req, res) => {
  const { postId } = req.params;
  res.json({ postId, comments: [] });
});

const postsRouter = express.Router();
postsRouter.use('/:postId/comments', commentsRouter);

app.use('/posts', postsRouter);
// GET /posts/42/comments -> postId is "42"

{ mergeParams: true } is what lets commentsRouter see req.params.postId, which was captured by the parent router's :postId segment.

Query Parameters for Filtering

GET /products?category=electronics
GET /products?sort=price&order=desc
GET /products?page=2&limit=20
  • Filtering: ?category=electronics narrows a collection without changing the resource itself.
  • Sorting: ?sort=price&order=desc controls result ordering.
  • Pagination: ?page=2&limit=20 limits how many results are returned per request.
  • All of these live in req.query, parsed automatically by Express from the query string.

API Routing Patterns Cheatsheet

Common patterns beyond basic single-resource CRUD.

Pattern Example URL
Nested resource /posts/:postId/comments
Filtering /products?category=electronics
Sorting /products?sort=price&order=asc
Pagination /products?page=2&limit=20
Search /products?q=keyboard
Field selection /products?fields=id,name

Implementing Pagination

Returning an entire collection at once doesn't scale as data grows. Pagination lets clients request a manageable slice at a time, using page/limit (offset-based) or a cursor-based approach for very large or frequently-changing datasets.

app.get('/products', (req, res) => {
  const page = Number(req.query.page) || 1;
  const limit = Number(req.query.limit) || 20;
  const start = (page - 1) * limit;

  const results = allProducts.slice(start, start + limit);

  res.json({
    page,
    limit,
    total: allProducts.length,
    data: results,
  });
});

Including total in the response lets clients calculate how many pages exist without a separate request.

Combining Multiple Filters Safely

As more query parameters are supported, validate and apply them incrementally rather than trusting arbitrary input directly, especially before passing filter values into a database query.

  • Whitelist which query parameters your endpoint actually supports.
  • Validate types and ranges (e.g. limit shouldn't be negative or absurdly large).
  • Never interpolate raw query parameters directly into a SQL string, see the Request Validation and Databases lessons.

Common Mistakes

  • Returning an entire large collection with no pagination support at all.
  • Forgetting { mergeParams: true } on a nested router, losing access to a parent route's parameters.
  • Trusting query parameters blindly without validating their type or range before using them.
  • Mixing filtering logic directly into route handlers instead of a reusable query-building helper.

Key Takeaways

  • Nested routes express "belongs to" relationships directly in the URL path.
  • { mergeParams: true } lets nested routers access a parent router's route parameters.
  • Filtering, sorting, and pagination are expressed through query parameters, not the path.
  • Always validate and whitelist query parameters before using them to build data queries.

Pro Tip

Always return pagination metadata (page, limit, total) alongside paginated results, even if your first client doesn't use it yet. Adding it retroactively is a breaking change; including it from the start costs almost nothing.