DEV Community

PRABHANSH TIWARI
PRABHANSH TIWARI

Posted on

🛣️ Routing in Backend Development — The Complete Developer's Guide

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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  ─┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

Heads up: Even if the value looks like a number (123), it arrives as a string in req.params. Always parse it if you need a number: parseInt(req.params.id) or Number(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" }
Enter fullscreen mode Exit fullscreen mode

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 ?)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

v2 needs to rename name to title (breaking change for anyone reading the name field):

{ "id": 1, "title": "Laptop", "price": 999 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.`
  });
});
Enter fullscreen mode Exit fullscreen mode

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  ✅
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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. 🚀

Top comments (0)