Backend

Routing Explained: How Servers Know Where Your Request Goes

Code to Ship
2026-02-03
8 min read

When a request reaches a server, one simple question decides everything.

“Where should this request go, and what should happen next?”

That decision is handled by routing.

Routing isn’t a framework feature. It isn’t magic. It’s a very deliberate system that takes your intent and your destination, matches them together, and hands them to the correct server-side logic.

This article breaks routing down exactly how it works in real backend systems.

Routing Starts With Intent

Every HTTP request carries intent. That intent is expressed using the HTTP method.

  • GET -> fetch data
  • POST -> create data
  • PUT -> replace data
  • PATCH -> partially update data
  • DELETE -> remove data

Methods answer the question: What do you want to do?

But intent alone isn’t enough. The server also needs to know: Where do you want to do it?

That’s where routing comes in.

What Routing Actually Does

Routing defines where your intent should be applied.

A route is the address of a resource on the server.

Example:

GET /users

Here’s what you’re telling the server:

  • What -> GET (fetch)
  • Where -> /users (the users resource)

The server takes:

  • The method (GET)
  • The route (/users)

And maps them to a specific handler -> It’s a piece of server side logic that knows exactly what to do.

Routing is essentially: Mapping request intent + URL to server-side logic

Static Routes

A static route never changes.

Example:

GET /api/books
  • No variables
  • No dynamic values
  • Always returns the same type of data

This route always points to the same handler and always represents the same resource: books.

Static routes are predictable and easy to reason about.

Same Route, Different Intent

Routes don’t exist alone. They are always paired with methods.

Example:

GET /api/books
POST /api/books

Even though the route is the same:

  • GET /api/books -> fetch all books
  • POST /api/books -> create a new book

Inside the server, method + route together form a unique key. They don’t clash.

The server checks:

  1. The HTTP method
  2. The route path

Only then does it decide which handler to execute.

How Servers Match Routes Internally

Conceptually, routing works like this:

(method, route) → handler

So:

  • (GET, /api/books) → fetchBooksHandler
  • (POST, /api/books) → createBookHandler

Same path. Different intent. Different logic.

Dynamic Routes (Path Parameters)

Not all routes are static. Sometimes part of the route represents data.

Example:

GET /api/users/123

Here:

  • 123 is not part of the route structure
  • It represents a specific user ID

This is called a path parameter (or route parameter).

Why Path Parameters Exist

Path parameters provide semantic meaning.

/api/users/:id

This clearly communicates: “Fetch the user whose ID is X”

The server extracts the value (123) and passes it to the handler, which can:

  • Query the database
  • Perform Business logic
  • Return the specific user

Pseudocode Example:

r.GET("/api/users/:id", handler)

This tells the server:

  • Match GET requests
  • Match /api/users/anything
  • Treat the last segments as id

Query Parameters

Sometimes you want to send extra data with a request, without changing the meaning of the resource.

Example:

GET /api/search?query=phone

Everything after ? is a query parameter.

Why Not Use Path Parameters For Everything?

Path parameters are for semantic identification.

Example:

/api/users/123

This has a very specific meaning.

Query parameters are for:

  • Filtering
  • Sorting
  • Pagination
  • Optional values

They are key-value pairs. Using path parameters for random data would make APIs hard to maintain and unclear.

Common Use cases for Query Parameters

Pagination:

GET /api/products?page=2

Filtering:

GET /api/products?category=books

Sorting:

GET /api/products?sort=price

Nested Routes

Resources are often related. Nested routes express that relationship.

Example:

GET /api/users/123/posts

Meaning: “Get posts that belong to user 123”

Going deeper:

GET /api/users/123/posts/456

Now you’re requesting: “Post 456 that belongs to user 123”

Each level adds context.

Each Nested Route Is Its Own Handler

These are all separate routes:

  • /api/users
  • /api/users/:id
  • /api/users/:id/posts
  • /api/users/:id/posts/:postId

Each maps to a different handler and different logic.

Route Versioning

APIs evolve. When response formats change, you need a safe way to support existing clients.

Example:

GET /api/v1/products
GET /api/v2/products

Why Version Routes?

Let’s say:

  • v1 returns: id, name, price
  • v2 returns: id, title, price

Old clients keep working. New clients use the new structure. No breaking changes.

Deprecation Workflow

  1. Release v2
  2. Notify frontend or mobile teams
  3. Allow a migration window
  4. Deprecate v1
  5. Eventually remove it

This creates stable and predictable API evolution.

Catch-All Routes

What happens when a request hits a route that doesn’t exist?

Example:

GET /api/v3/products

If the server has no handler, the request falls through.

Catch-All Handling

Servers usually define a final route: /*

This catches everything that wasn’t matched earlier. Instead of returning nothing, the server responds with a clear message:

  • Route not found
  • Helpful error response

This improves developer experience and debugging.

Final Thoughts

Routing is simple in concept, but powerful in practice.

It connects:

  • Intent (HTTP method)
  • Destination (URL)
  • Logic (handler)

Once you understand routing, APIs stop feeling abstract. They become structured, predictable systems that are easy to extend.