Backend from First Principles

Topic 2 of 4

Routing

How Express reads a URL

Interview answer

The URL says which resource, the method says what to do with it. Express uses the path to find the matching route, puts :params into req.params, and puts everything after ? into req.query. All values are strings.

One request through the router
ClientExpress routerHandler

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Step through the diagram to see requests and responses.

Static routes

Interview answer

A static route is a fixed path, like /api/products. It always means the same thing: "give me all products".

Other examples: /api/users, /api/orders, /api/categories.

js
app.get('/api/products', (req, res) => {
  res.json(products) // GET /api/products → "give me all products"
})

Dynamic routes (path parameters)

Interview answer

A dynamic route has a placeholder, like /api/products/:productId. It matches /api/products/123, and Express puts "123" in req.params.productId. It's for one specific item.

js
app.get('/api/products/:productId', (req, res) => {
  const productId = req.params.productId // GET /api/products/123 → "123" (a string)
  res.json({ productId })
})

Query parameters

Interview answer

Query parameters come after the ?, like ?category=shoes&page=2. They're for filtering, sorting and pagination. Express puts them in req.query, and they are not used to pick the route.

Path parameterQuery parameter
Example/products/123/products?page=2&sort=price
AnswersWhich product?How do I want them?
Expressreq.paramsreq.query
Used for matching?YesNo
js
// GET /api/products?category=shoes&page=2&limit=20&sort=price
app.get('/api/products', (req, res) => {
  const { category, page, limit, sort } = req.query // all strings: page === "2"
})

Nested routes

Interview answer

Nested routes show a parent-child relationship, like /users/123/orders for the orders of user 123. I keep them at most two levels deep.

If URLs start looking like /users/123/orders/456/items/789/reviews/999, rethink the resource design. /orders/456 or /reviews/999 usually works on its own.

js
app.get('/api/users/:userId/orders', (req, res) => {
  const { userId } = req.params // orders belonging to user 123
})

app.get('/api/users/:userId/orders/:orderId', (req, res) => {
  const { userId, orderId } = req.params // order 456 of user 123
})

API versioning

Interview answer

When I make a breaking change, like renaming a field, I put it in /api/v2 and keep /api/v1 working, so old apps don't break. Once everyone has moved, I remove v1.

Old and new clients side by side

Old frontend

ClientServer

New frontend

ClientServer

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Old frontend
Step through the diagram to see requests and responses.
New frontend
Step through the diagram to see requests and responses.
js
app.get('/api/v1/products', getProductsV1)
app.get('/api/v2/products', getProductsV2)

Catch-all 404 and route order

Interview answer

Express checks routes from top to bottom and uses the first match. So the 404 catch-all app.use() must be the very last one; if it's first, it catches every request.

Route matcher

  1. app.get('/api/v1/products', listProducts)
  2. app.get('/api/v1/products/:productId', getProduct)
  3. app.get('/api/v1/products/:productId/reviews', getReviews)
  4. app.post('/api/v1/products', createProduct)
  5. app.patch('/api/v1/products/:productId', updateProduct)
  6. app.delete('/api/v1/products/:productId', deleteProduct)
  7. app.get('/api/v1/users/:userId/orders', getUserOrders)
  8. app.get('/api/v1/users/:userId/orders/:orderId', getUserOrder)
  9. app.use((req, res) => res.status(404)…)
js
app.get('/api/products', getProducts)
app.get('/api/users', getUsers)
app.get('/api/orders', getOrders)

// keep this at the bottom: "if nothing above handled it, handle it here"
app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' })
})

Real-world example: e-commerce

Interview answer

One small app that uses everything: a static route with query params, a dynamic route, a nested route, create, update, delete, and the 404 catch-all at the end.

Define each route once. Don't register GET /api/v1/products twice; combine the logic in one handler.

js
const express = require('express')
const app = express()
app.use(express.json())

app.get('/api/v1/products', (req, res) => { // static + query
  const { category, page, limit } = req.query
  res.json({ category, page, limit })
})

app.get('/api/v1/products/:productId', (req, res) => { // dynamic
  res.json({ message: `Product ${req.params.productId}` })
})

app.get('/api/v1/products/:productId/reviews', (req, res) => { // nested
  res.json({ message: `Reviews for ${req.params.productId}` })
})

app.post('/api/v1/products', (req, res) => { // create
  res.status(201).json({ message: 'Product created' })
})

app.patch('/api/v1/products/:productId', (req, res) => { // partial update
  res.json({ message: `Product ${req.params.productId} updated` })
})

app.delete('/api/v1/products/:productId', (req, res) => { // delete
  res.status(204).send()
})

app.use((req, res) => { // catch-all, last
  res.status(404).json({ error: 'Route not found' })
})

app.listen(3000)

Best practices

Interview answer

Nouns, not verbs (GET /users, not /getUsers). Plural names. Clear parameter names like :productId. Query params for filters. PATCH for partial updates. Shallow nesting. The 404 handler last.

RuleGoodBad
Nouns, not verbsGET /users, DELETE /users/123/getUsers, POST /deleteUser/123
Plural names/users, /products/user/123 and /products/456 mixed
Meaningful params/products/:productId/products/:id
Filters in the query/products?category=shoes/products/shoes
PATCH vs PUTPATCH /products/123 { "price": 900 }PUT with one field
Shallow nesting/users/123/orders/users/123/orders/456/items/789/reviews
Consistent casing/users/123/Users/123
Don't hide operationsDELETE /products/123/products/123?delete=true
404 lastcatch-all at the bottomcatch-all before real routes

Quiz

Think of the answer first, then reveal it.

0/10 answers revealed
  1. Q1 · concept

    What's the difference between a path parameter and a query parameter? Where does Express put each?

  2. Q2 · concept

    Is the query string used when Express matches a route?

  3. Q3 · concept

    What type is req.query.page for ?page=2?

  4. Q4 · scenario

    Every request to your API returns 404, even valid ones. You just added a catch-all. What's wrong?

  5. Q5 · concept

    Why use API versioning like /v1 and /v2?

  6. Q6 · scenario

    Review this API: GET /getUsers, POST /createUser, POST /deleteUser/123. How would you fix it?

  7. Q7 · concept

    When do you use PATCH vs PUT?

  8. Q8 · scenario

    A teammate designs /users/123/orders/456/items/789/reviews/999. What do you suggest?

  9. Q9 · scenario

    Should filtering be /products/shoes or /products?category=shoes? Why?

  10. Q10 · concept

    A client sends DELETE /api/v1/products (no ID) and you only defined GET and POST on that path. What does Express return?