How Express reads a URL
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.
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
Static routes
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.
app.get('/api/products', (req, res) => {
res.json(products) // GET /api/products → "give me all products"
})Dynamic routes (path parameters)
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.
app.get('/api/products/:productId', (req, res) => {
const productId = req.params.productId // GET /api/products/123 → "123" (a string)
res.json({ productId })
})Query parameters
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 parameter | Query parameter | |
|---|---|---|
| Example | /products/123 | /products?page=2&sort=price |
| Answers | Which product? | How do I want them? |
| Express | req.params | req.query |
| Used for matching? | Yes | No |
// 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
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.
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
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 frontend
New frontend
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
Step through the diagram to see requests and responses.
app.get('/api/v1/products', getProductsV1)
app.get('/api/v2/products', getProductsV2)Catch-all 404 and route order
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
app.get('/api/v1/products', listProducts)app.get('/api/v1/products/:productId', getProduct)app.get('/api/v1/products/:productId/reviews', getReviews)app.post('/api/v1/products', createProduct)app.patch('/api/v1/products/:productId', updateProduct)app.delete('/api/v1/products/:productId', deleteProduct)app.get('/api/v1/users/:userId/orders', getUserOrders)app.get('/api/v1/users/:userId/orders/:orderId', getUserOrder)app.use((req, res) => res.status(404)…)
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
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.
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
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.
| Rule | Good | Bad |
|---|---|---|
| Nouns, not verbs | GET /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 PUT | PATCH /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 operations | DELETE /products/123 | /products/123?delete=true |
| 404 last | catch-all at the bottom | catch-all before real routes |
Quiz
Think of the answer first, then reveal it.
- Q1 · concept
What's the difference between a path parameter and a query parameter? Where does Express put each?
- Q2 · concept
Is the query string used when Express matches a route?
- Q3 · concept
What type is
req.query.pagefor?page=2? - Q4 · scenario
Every request to your API returns 404, even valid ones. You just added a catch-all. What's wrong?
- Q5 · concept
Why use API versioning like /v1 and /v2?
- Q6 · scenario
Review this API:
GET /getUsers,POST /createUser,POST /deleteUser/123. How would you fix it? - Q7 · concept
When do you use PATCH vs PUT?
- Q8 · scenario
A teammate designs
/users/123/orders/456/items/789/reviews/999. What do you suggest? - Q9 · scenario
Should filtering be
/products/shoesor/products?category=shoes? Why? - 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?