You've probably typed GET /api/users a hundred times. But do you really know what's happening under the hood when a request hits your server and somehow finds its way to the right function?
Routing is one of those fundamentals that every backend dev uses daily but rarely sits down to understand deeply. This guide fixes that. We'll go from the basics all the way to versioning, middleware, and the unsung hero — the catch-all route.
Let's get into it.
🧠 What Even Is Routing?
At its core, routing is the system that maps an incoming HTTP request to the correct piece of server-side logic (called a handler) that should process it.
Think of it like a post office. A letter (request) comes in. The post office (router) reads two things: what kind of delivery is this and where is it going. Then it hands the letter to the right person (handler).
Every route is defined by two things:
| Part | Expressed By | Meaning |
|---|---|---|
| What do you want to do? | HTTP Method (GET, POST, PUT, DELETE) |
Your intent |
| Where do you want to do it? | URL Path (/api/books) |
The resource |
GET /api/users
│ │
│ └── WHERE: the users resource
└── WHAT: I want to read data
The server combines both pieces and fires the right handler — which runs business logic, talks to the database, and returns a response.
Simple enough. But routing gets a lot more nuanced from here.
1. Static Routes — The Foundation
A static route is the simplest kind — the URL path is fixed, constant, never changes.
GET /api/books → Returns a list of all books
POST /api/books → Creates a new book
Notice something? Both use /api/books but they're two completely different routes because the HTTP method is different. The method is what differentiates them.
GET /api/books ─┐
├── TWO different routes
POST /api/books ─┘
Static routes are the bread and butter. They're predictable, easy to document, and easy to test. Always reach for them when you're working with a collection of resources.
2. Dynamic Routes & Path Parameters — Targeting Specific Resources
Static routes are great for collections. But what if you want data for one specific user? You don't want all 50,000 users — just the one with ID 123.
That's where dynamic routes come in. You embed a variable directly in the URL path:
GET /api/users/123
└── This is the dynamic part — the user's ID
On the server side, you define this with a path parameter (also called a route parameter):
// The colon (:) before "id" tells the router:
// "treat this segment as a variable, accept any value here"
router.get("/api/users/:id", (req, res) => {
const userId = req.params.id; // "123" when hitting /api/users/123
// Fetch this specific user from the DB
const user = db.users.findById(userId);
res.json(user);
});
Heads up: Even if the value looks like a number (
123), it arrives as a string inreq.params. Always parse it if you need a number:parseInt(req.params.id)orNumber(req.params.id).
You can also have multiple path parameters in a single route:
router.get("/api/users/:userId/posts/:postId", handler);
// /api/users/42/posts/7 → { userId: "42", postId: "7" }
3. Query Parameters — Filtering, Sorting, Pagination
Here's something that trips up a lot of junior devs: GET requests don't have a body. So how do you send extra data like "give me page 2" or "filter by active users"?
You use query parameters — key-value pairs tacked onto the end of a URL after a ?.
/api/books?page=2&limit=20&sort=asc
└─────────────────────┘
query parameters (after ?)
Multiple params are separated by &. Here are the most common real-world uses:
# Pagination
GET /api/books?page=2&limit=20
# Filtering
GET /api/users?status=active&role=admin
# Sorting
GET /api/products?sort=price&order=desc
# Search
GET /api/search?query=javascript+closures
On the server side, you access them via req.query:
router.get("/api/books", (req, res) => {
const { page = 1, limit = 10, sort = "createdAt" } = req.query;
const books = db.books.findAll({ page, limit, sort });
res.json(books);
});
Path Param vs Query Param — The Rule of Thumb
This is one of the most common sources of confusion. Here's the mental model:
/api/users/123/posts?page=2&sort=desc
│ │
│ └── Query Params → extra metadata
│ (filters, pagination, sorting, search)
│
└── Path Param → part of the resource identity
(which specific user? user 123)
| Use Case | Use |
|---|---|
| Identifying a specific resource | Path param: /users/123
|
| Filters, sorting, pagination, search | Query param: ?status=active
|
If you tried using path params for search:
/api/search/javascript+closures ❌ Weird, hard to maintain
/api/search?query=javascript ✅ Clean, correct, idiomatic
4. Nested Routes — Expressing Relationships
APIs often deal with resources that belong to other resources. A post belongs to a user. A comment belongs to a post. How do you express that in your URLs?
With nested routes — chaining resources together in the path to show hierarchy:
/api/users/123/posts/456
│ │ │ │
│ │ │ └── Specific post (ID 456)
│ │ └── "posts" resource (child of user)
│ └── Specific user (ID 123)
└── "users" resource
Each level maps to a completely different handler:
router.get("/api/users", getAllUsers);
// → Returns all users
router.get("/api/users/:userId", getUserById);
// → Returns user 123
router.get("/api/users/:userId/posts", getPostsByUser);
// → Returns all posts *by* user 123
router.get("/api/users/:userId/posts/:postId", getSinglePost);
// → Returns the specific post 456 by user 123
Nested routes make your API self-documenting. A developer reading /api/orders/89/items/3 immediately understands: "item 3 from order 89." No documentation needed.
A word of caution though — don't go deeper than 2–3 levels. Beyond that, URLs become unwieldy and hard to work with:
/api/orgs/1/teams/5/projects/12/tasks/89/comments/3 ❌ Too deep
/api/tasks/89/comments/3 ✅ Flatten it
5. HTTP Methods — More Than Just GET and POST
Most devs know GET and POST, but a well-designed REST API uses the full suite of HTTP methods to communicate intent:
| Method | Purpose | Example |
|---|---|---|
GET |
Read / fetch data | GET /api/users |
POST |
Create a new resource | POST /api/users |
PUT |
Replace a resource entirely | PUT /api/users/123 |
PATCH |
Partially update a resource | PATCH /api/users/123 |
DELETE |
Remove a resource | DELETE /api/users/123 |
The difference between PUT and PATCH catches a lot of people:
// PUT — you send the ENTIRE object, it replaces everything
PUT /api/users/123
Body: { "name": "John", "email": "john@new.com", "role": "admin" }
// PATCH — you send only the fields you want to change
PATCH /api/users/123
Body: { "email": "john@new.com" }
Using the right HTTP method isn't just semantics — it affects caching, idempotency, and how frontend developers consuming your API will reason about it.
6. Route Versioning — Managing Breaking Changes Gracefully
Here's a scenario every backend dev will face eventually. Your API is live. Clients (mobile apps, frontend teams, third-party integrations) are consuming it. And now you need to change the shape of a response in a way that would break existing clients.
What do you do? You version your routes:
GET /api/v1/products → Old response format
GET /api/v2/products → New response format
Both live simultaneously. Old clients keep using v1 without breaking. New clients adopt v2 on their own timeline.
Here's a concrete example. Your v1 endpoint returns:
{ "id": 1, "name": "Laptop", "price": 999 }
v2 needs to rename name to title (breaking change for anyone reading the name field):
{ "id": 1, "title": "Laptop", "price": 999 }
The Versioning Workflow
1. Ship v2 alongside v1 (both work simultaneously)
↓
2. Announce to all consumers: "v1 is now DEPRECATED"
↓
3. Give teams a migration window (2–4 weeks is typical)
↓
4. All clients migrate from v1 → v2
↓
5. Shut down v1
This is professional, considerate API design. It's the difference between a reliable API and one that breaks people's apps on a Tuesday morning.
7. Middleware — The Unsung Hero of Routing
Routing doesn't just mean "request hits handler." In real apps, requests pass through a pipeline of middleware functions before reaching the handler.
Middleware is a function that sits between the router and your handler. It can:
- Authenticate the request (is this user logged in?)
- Log the request
- Parse the request body
- Validate input
- Rate-limit the caller
- And more...
// Middleware runs BEFORE the handler
const authenticate = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: "Unauthorized" });
}
req.user = verifyToken(token); // attach user to request
next(); // pass control to the next middleware or handler
};
// Apply middleware to a specific route
router.get("/api/profile", authenticate, (req, res) => {
// req.user is available here because middleware set it
res.json(req.user);
});
// Or apply to all routes in a group
router.use("/api/admin", authenticate, adminRateLimiter);
Think of middleware as a series of gates a request must pass through. Any gate can stop the request (return an error) or let it continue (next()). This keeps your handlers clean and focused on business logic.
8. The Catch-All Route — Your Safety Net
What happens when someone hits a route that doesn't exist? Like /api/v3/products on a server that only has v1 and v2?
Without a catch-all, the server might return an empty response, a confusing framework error, or even crash in some setups. Not great.
The catch-all route is your last line of defense. Define it at the very end of all your routes:
// All real routes defined first
router.get("/api/v1/products", getProductsV1);
router.get("/api/v2/products", getProductsV2);
router.get("/api/users", getAllUsers);
// ... all other routes ...
// Catch-all — MUST be last
// The asterisk (*) matches anything that hasn't been caught above
router.all("*", (req, res) => {
res.status(404).json({
error: "Route not found",
message: `The endpoint '${req.url}' does not exist on this server.`
});
});
Why must it be last? Because routes are evaluated in order. If you put the catch-all first, it swallows every request before any real routes get a chance to match.
A good 404 response is developer-friendly — tell them what wasn't found. It saves hours of debugging.
9. RESTful Route Design Patterns — The Conventions You Should Follow
When designing routes, there are widely accepted conventions that make your API feel familiar to any developer:
# ✅ Use nouns for resources, not verbs
GET /api/users ✅
GET /api/getUsers ❌ (verb in URL)
# ✅ Use plural nouns
GET /api/users ✅
GET /api/user ❌ (inconsistent)
# ✅ Use lowercase and hyphens, not camelCase
GET /api/blog-posts ✅
GET /api/blogPosts ❌
# ✅ Actions on a resource use HTTP methods, not URL verbs
DELETE /api/users/123 ✅
GET /api/users/123/delete ❌
# ✅ Non-CRUD actions? Use a sub-resource or action endpoint
POST /api/orders/89/cancel ✅
POST /api/payments/55/refund ✅
Following these conventions means any developer reading your API docs will immediately understand the structure, even if they've never seen your code before.
10. Quick Reference
Here's everything in one place:
| Concept | Example | Use For |
|---|---|---|
| Static Route | GET /api/books |
Fixed resource, always the same path |
| Dynamic Route | GET /api/users/:id |
Targeting a specific resource |
| Path Parameter |
/users/123 → :id = "123"
|
Resource identity |
| Query Parameter | ?page=2&sort=desc |
Filters, pagination, sorting, search |
| Nested Route | /users/123/posts/456 |
Parent-child resource relationships |
| Route Versioning |
/api/v1/ vs /api/v2/
|
Breaking changes without breaking clients |
| Middleware |
authenticate, rateLimit
|
Cross-cutting concerns before the handler |
| Catch-All Route | /* |
Graceful 404 for unknown routes |
The Mental Model to Remember
Incoming Request
│
▼
Middleware Pipeline
(auth, logging, validation...)
│
▼
Router
(matches method + path)
│
┌──┴──────────────────────────────────────────┐
│ /api/v2/users/:id/posts?page=2&sort=desc │
│ │ │ │
│ Path Param Query Params │
│ (specific resource) (request metadata) │
└─────────────────────────────────────────────┘
│
▼
Handler
(business logic + DB)
│
▼
Response
Routing is the backbone of every web API. Get these fundamentals right, and you'll build APIs that are predictable, maintainable, and a joy for other developers to consume.
Happy building. 🚀
This article was originally published by DEV Community and written by PRABHANSH TIWARI.
Read original article on DEV Community