A Detailed Backend Reference

Routing, the where
of every request.

HTTP methods describe the what of a request — your intent. Routing describes the where: which resource on the server your intent is aimed at. This manual covers routing end to end — static and dynamic routes, path and query parameters, nesting, versioning, and catch-alls — with worked routers in Go and Python.

the where of a request method + path → handler 13 sections
Part I — The ModelThe core idea
01

What Routing Is

Every request carries two things bundled together. The HTTP method answers “what do I want to do?” — fetch, add, update, or delete data. That is your intent, your action. The route answers “where do I want to do it?” — which resource, at which address on the server, you want to perform that action on.

You have to tell the server where to go. Send a GET request (intent: fetch) to the route /users (the address), and the server replies with an array of users. Routing is the machinery that takes the what and the where, maps them to a particular handler, and lets that handler run the business logic and return the data.

Routing — definition

Routing is mapping URL paths (plus the HTTP method) to server-side logic — a handler that performs the business logic, database operations, and whatever else is needed, then returns a response. That is all there is to it.

The words used in this manual

  • Intent / action — what the request wants to do, expressed by the HTTP method (fetch, add, update, delete).
  • Resource — the “thing” the request acts on, usually a noun in the path such as books or users. (Resources are studied properly under REST APIs; here we treat them simply as the route’s target.)
  • Route / route path / URL path — the address part that says where on the server you are going, e.g. /api/books.
  • Handler — the set of instructions the server runs when a request matches a route; it owns the logic for that endpoint.
02

Method + Path → Handler

This is the heart of routing. The server takes the method and the route and treats the two together as a unique key that maps to one handler. It checks the method first, then the route, concatenates them, and forms a unique routing path.

Consider two requests to the same address. GET /api/books fetches the books; POST /api/books creates one. The route is identical, but the method differs — so the keys are different and they route to different handlers. The methods differentiate between two otherwise identical routes, and the keys never clash.

GET /api/books intent: fetch the books POST /api/books intent: add a book DELETE /api/books/9 intent: remove book 9 ROUTER method + path = unique key GET /books POST /books DEL /books/:id listBooks() → returns all books createBook() → creates, returns books deleteBook(id) → removes the book
One key, one handler. The server checks method, then route, concatenates them into a unique key, and dispatches to the matching handler. Same path + different method = different destination.
The method is the “what” (brief)

Routing only needs to know the method as the other half of the key. The common verbs and their intent: GET fetch · POST add/create · PUT / PATCH update · DELETE remove. The deep study of methods belongs to the HTTP topic; here they simply pair with a route to form the key.

Part II — Route TypesShapes a route can take
03

Static Routes

A static route is a fixed path with no variable parameters inside it. /api/books is static: the string stays consistent — it is a constant. You always use that same string when you make the request, nothing in it changes, and it always returns the same kind of response. That is exactly why it is called “static.”

Static route

A constant path string with no dynamic segments, e.g. /api/books. The first two examples from the source — GET and POST on /api/books — are both static routes.

Matching a static route is a literal string comparison. Most APIs begin here: one endpoint that lists a collection and one that creates an item, both on the same constant path.

04

Dynamic Routes & Path Parameters

A dynamic route has a variable parameter inside the path, so one pattern can serve many specific resources. GET /api/users/123 fetches the details of one user — the 123 is the user’s ID, which the server extracts from the route to do its work (fetch that user from the database and return them).

On the server side, the route definition for this looks like a method plus a pattern. Written out, the matching part is something like:

route definitionpattern
r.get("/api/users/:id", handler)
        //          └── ":id" = a dynamic parameter slot

The method part matches GET; the path part matches /api/users/ followed by any string. The colon convention — :id — says “this segment is dynamic; the client supplies a value here.” You will find this :name convention across Node, Python, Go, Rust, and Java; it is an industry-wide practice.

/api/users/ :id pattern (the route definition) /api/users/123 /api/users/abc /api/users/9f2 each binds → id = "123" | "abc" | "9f2"
One pattern, many matches. The dynamic segment :id matches any string in that position and binds it to a named value the handler reads.
Path parameter (route parameter)

A dynamic value that lives inside the path, right after a forward slash. Because it is part of the route, it is called a path parameter or route parameter, and it carries semantic meaning — it identifies which resource the request is about.

  • Everything in a path arrives as a string. 123 looks like a number, but in route paths numbers and special characters are all treated as strings; the handler casts it if needed.
  • Read aloud, GET /api/users/123 says “fetch the data of the user whose ID is 123.” That human-readable construct is the whole idea behind REST-style routing — it gives routes a clear semantic meaning.
05

Query Parameters

A query parameter is a key/value pair attached to the end of the route after a question mark. Multiple pairs are joined by an ampersand. For example, /api/search?query=some+value — the route is /api/search, and after the ? comes the key query with the value some+value.

/api/search ? query = some+value & page = 2 & limit = 20 the route ? begins the query keys values & joins pairs
Structure. First pair after ?, the rest after &, each a key=value. A space in a value is written as +.

Why query parameters exist

POST and PUT requests have a body — a container for sending user-defined data to the server. But GET requests, by REST convention, have no body. So how does a GET send extra values? Query parameters are the answer: they let a GET ship a set of key/value pairs carrying metadata about the request — not the identity of the resource, but instructions for shaping the response.

Query parameter

An optional key=value pair in the query string, used to filter, sort, paginate, or search. Typically used with GET requests because they have no body. Optional, repeatable, and order-independent.

The main example: pagination

Imagine an endpoint that returns a list of books in paged format. The first call, /api/books, takes the server defaults and returns a chunk of data plus some metadata describing the whole set:

JSON · responseGET /api/books
{
  "data": [ /* …the first 20 books (the default page)… */ ],
  "total":        100,   // how many books exist in total
  "currentPage":  1,     // which page this is (default = 1)
  "limit":        20,    // page size (default limit)
  "totalPages":   5      // 100 / 20 = 5 pages
}

The client uses this response metadata to make the next request. The default needed no parameter; to get page two the client sends /api/books?page=2. The same mechanism carries filters (e.g. filter by a user-defined value) and sort order (ascending or descending) — all as key/value pairs in the query.

06

Path vs Query Parameters

Both put data in the URL, and it is easy to confuse them, so it is worth being precise. They serve different purposes.

/api/users/123 ?sort=name&page=2 PATH PARAMETER Identifies WHICH resource. Part of the route. Semantic. e.g. user whose ID is 123. QUERY PARAMETER Shapes HOW it's returned. After the route. Metadata. filter · sort · paginate · search.
Rule of thumb. If it decides which thing you get → path parameter. If it only changes the shape or subset of the response → query parameter.

Path parameters serve a particular purpose: a semantic expression of which resource — “the user whose ID is 123.” You could technically push a search value into the path (/api/search/some+value), but it is hard to maintain and defeats the whole purpose of giving routes a clear semantic meaning. Metadata like a search term belongs in the query string instead: /api/search?query=some+value.

07

Nested Routes

Nesting is not really a separate type of route — it is a practice you will see everywhere. In REST-style APIs, you often nest different resources to express a semantic relationship. Each deeper level narrows the meaning and maps to its own handler, returning a different granularity of data.

/api/users → list all users /api/users/123 → one user /api/users/123/posts → that user's posts /api/users/123/posts/456 → one post
Each level is its own endpoint. Stop at any rung and you get a different response: all users, one user, that user's posts, or one specific post.

Walking the deepest example, /api/users/123/posts/456:

  • /api/users — a static part; on its own it returns the list of all users.
  • 123 — a path parameter for the user ID; /api/users/123 returns that one user.
  • posts — a static part naming a sub-collection; /api/users/123/posts returns all posts of that user.
  • 456 — a path parameter for a specific post; the full route fetches “the post with ID 456 belonging to the user with ID 123.”

That is why it is called a nested route — you nest different types of information at different levels to express different semantic meanings, and each produces a different response. You will see this used pretty much everywhere once an API has even medium complexity.

Part III — Lifecycle & EvolutionAt run-time & over time
08

The Routing Lifecycle

Putting the pieces together, here is the path a single request travels — the routing step in context.

1 · ARRIVE GET /api/users/123 2 · READ split route / query 3 · MATCH method+route → route 4 · EXTRACT id = 123 5 · HANDLER business logic · DB 6 · RESPOND return the data
Six beats. Arrive → read the route → match (the routing step) → extract parameters → run the handler → return the response.
09

Versioning & Deprecation

Route versioning is a very common practice for managing change in API endpoints. You put a version keyword in the route — /api/v1/products and /api/v2/products. It looks like an ordinary route with a v1 or v2 segment added.

Why bother? Suppose v1 returns each product as { id, name, price }. Later, new requirements arrive — say you now serve a mobile app and must change the response shape, renaming name to title. If you mutate the live route, you break every existing client. Versioning lets both shapes live at once:

/api/v1/products/api/v2/products
{ id, name, price }{ id, title, price }

This does two things. It expresses intent clearly — v1 of the response and v2 of the response side by side. And it means you did not have to change the whole route (you didn’t need a /api/new-products); you just bumped the version.

time → /api/v1/products — live /api/v2/products — live MIGRATION WINDOW v2 ships v1 deprecated → removed
The safe workflow. v2 ships beside v1; clients get a window to migrate; then v1 is deprecated and removed. Eventually v2 may be promoted to the default.
Deprecation

Announcing that an endpoint (e.g. v1) is going away once v2 is released. Engineers get a defined migration window to move their requests over to v2; only after that does the team remove v1. Versioning plus deprecation gives a stable workflow for shipping breaking changes without breaking clients in the moment.

10

Catch-All Routes

A catch-all route handles requests for routes the server does not actually serve. Suppose a request comes in for /api/v3/products but there is no handler for it. The server places a catch-all last, after all the specific routes and methods — typically written with a wildcard, /*. Any request that falls through every earlier match lands there.

incoming: GET /api/v3/products (no handler exists) match GET /api/v1/products ? match GET /api/v2/products ? match GET /api/users/:id ? match /* (catch-all) friendly 404 "this route does not exist"
Fall-through. Because it sits last, the catch-all only fires when nothing else matched — turning a confusing default into a clear "not found".

Without catch-all handling, the default behaviour is often a null response, which is confusing. With it, the handler returns a user-friendly message — “this route does not exist” / “route not found” — so the client clearly understands the server does not cater to that endpoint.

Wildcard & ordering

The * wildcard matches anything, which is exactly why the catch-all must be registered last. Placed early, it would intercept every request before the real routes get a turn. The reliable rule: specific routes first, the catch-all at the very end.

Part IV — ImplementationBuild a router, with the OOP showing
11

A Router in Go

A router is, at bottom, a small object-oriented dispatch problem: a table from “method + route” to a handler. Go expresses the four OOP pillars through interfaces (abstraction + polymorphism), unexported fields (encapsulation), and struct embedding (composition — Go’s form of inheritance). The comments tagged OOP point out each one.

Go 1.22+router.go
package main

import ("fmt"; "strings")

// ABSTRACTION: Handler is an interface — it names a capability
// ("be able to Handle a request") without saying how. Any type
// with a Handle method IS-A Handler. The router depends only on
// this contract, never on a concrete type.
type Handler interface {
    Handle(req Request) Response
}

type Request struct {
    Method string
    Path   string
    Params map[string]string // extracted path params (:id)
    Query  map[string]string // query params (?page=2)
}
type Response struct { Status int; Body string }

// ENCAPSULATION: "routes" is lowercase => unexported => private to
// the package. Outsiders cannot touch the table directly; they go
// through the methods we expose (Register / Dispatch).
type Router struct {
    routes map[string]Handler // key = "METHOD /route"
}

func NewRouter() *Router {
    return &Router{routes: make(map[string]Handler)}
}

// Register binds method + route -> handler (the dispatch table).
func (r *Router) Register(method, pattern string, h Handler) {
    r.routes[method+" "+pattern] = h // method + path = the unique key
}

// POLYMORPHISM: Dispatch calls h.Handle(req) without knowing which
// concrete type h is. Same call, different behaviour per handler.
func (r *Router) Dispatch(req Request) Response {
    for key, h := range r.routes {
        method, pattern, _ := strings.Cut(key, " ")
        if method == req.Method && match(pattern, req.Path, &req) {
            return h.Handle(req) // polymorphic dispatch
        }
    }
    return Response{404, "route not found"} // catch-all fallback
}

// match compares "/users/:id" against "/users/123", filling Params.
func match(pattern, path string, req *Request) bool {
    p, q := strings.Split(pattern, "/"), strings.Split(path, "/")
    if len(p) != len(q) { return false }
    for i := range p {
        if strings.HasPrefix(p[i], ":") { // dynamic segment
            req.Params[p[i][1:]] = q[i] // bind :id => "123"
        } else if p[i] != q[i] { // static segment must match exactly
            return false
        }
    }
    return true
}

// "INHERITANCE" via COMPOSITION: BaseHandler holds shared logic;
// concrete handlers EMBED it and reuse its methods.
type BaseHandler struct{ Name string }

func (b BaseHandler) log(req Request) {
    fmt.Printf("[%s] %s %s\n", b.Name, req.Method, req.Path)
}

// GetUser embeds BaseHandler (composition) and satisfies Handler.
type GetUser struct{ BaseHandler }
func (h GetUser) Handle(req Request) Response {
    h.log(req) // reused (inherited) behaviour
    return Response{200, "user id = " + req.Params["id"]}
}

type ListBooks struct{ BaseHandler }
func (h ListBooks) Handle(req Request) Response {
    h.log(req)
    page := req.Query["page"] // query param drives pagination
    if page == "" { page = "1" }
    return Response{200, "books page " + page}
}

func main() {
    r := NewRouter()
    r.Register("GET", "/api/users/:id", GetUser{BaseHandler{"users"}})
    r.Register("GET", "/api/books",     ListBooks{BaseHandler{"books"}})

    req := Request{Method: "GET", Path: "/api/users/123",
        Params: map[string]string{}, Query: map[string]string{}}
    fmt.Println(r.Dispatch(req).Body) // => user id = 123
}
12

A Router in Python

The same architecture in classic class syntax: an abstract base class for the contract (abstraction), real subclassing (inheritance), overridden methods (polymorphism), and a name-mangled private attribute (encapsulation).

Python 3.11+router.py
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from urllib.parse import urlsplit, parse_qs

# ABSTRACTION: Handler is an Abstract Base Class. @abstractmethod
# forces every subclass to define handle(). You cannot create a
# Handler directly — it is a pure contract.
class Handler(ABC):
    @abstractmethod
    def handle(self, req: "Request") -> "Response": ...

@dataclass
class Request:
    method: str
    path: str
    params: dict = field(default_factory=dict)  # path params (:id)
    query: dict = field(default_factory=dict)   # query params (?page=2)

@dataclass
class Response:
    status: int
    body: str

# INHERITANCE: BaseHandler is a concrete parent holding shared
# behaviour (logging). Subclasses inherit and reuse _log().
class BaseHandler(Handler):
    def __init__(self, name): self._name = name   # protected by convention
    def _log(self, req): print(f"[{self._name}] {req.method} {req.path}")

# POLYMORPHISM: each subclass OVERRIDES handle() differently, yet
# the router calls them all identically — handler.handle(req).
class GetUser(BaseHandler):           # IS-A BaseHandler IS-A Handler
    def handle(self, req):
        self._log(req)              # inherited from parent
        return Response(200, f"user id = {req.params['id']}")

class ListBooks(BaseHandler):
    def handle(self, req):
        self._log(req)
        page = req.query.get("page", ["1"])[0]   # query param
        return Response(200, f"books page {page}")

# ENCAPSULATION: self.__routes is name-mangled to _Router__routes,
# so it is effectively private. The public surface is register()
# and dispatch(); the table stays hidden.
class Router:
    def __init__(self):
        self.__routes: dict[tuple[str, str], Handler] = {}

    def register(self, method, pattern, handler):
        self.__routes[(method, pattern)] = handler  # method + path key

    def dispatch(self, method, url):
        parts = urlsplit(url)
        query = parse_qs(parts.query)               # ?a=1&b=2 -> dict
        for (m, pattern), handler in self.__routes.items():
            params = self.__match(pattern, parts.path)
            if m == method and params is not None:
                return handler.handle(
                    Request(method, parts.path, params, query))  # polymorphic
        return Response(404, "route not found")       # catch-all

    @staticmethod
    def __match(pattern, path):
        p = pattern.strip("/").split("/")
        q = path.strip("/").split("/")
        if len(p) != len(q): return None
        params = {}
        for seg, val in zip(p, q):
            if seg.startswith(":"):       # dynamic segment
                params[seg[1:]] = val    # bind :id -> "123"
            elif seg != val:            # static must match exactly
                return None
        return params

if __name__ == "__main__":
    r = Router()
    r.register("GET", "/api/users/:id", GetUser("users"))
    r.register("GET", "/api/books",     ListBooks("books"))
    print(r.dispatch("GET", "/api/users/123").body)   # user id = 123
    print(r.dispatch("GET", "/api/books?page=2").body) # books page 2
Same architecture, two languages

Go reaches it through interfaces + embedding; Python through ABCs + class inheritance. Both are a private dispatch table keyed on method + route, an abstract handler contract, polymorphic dispatch, and a catch-all fallback — the whole routing model from this manual, in code.

13

Routing Glossary

Every routing term used above, in one place.

TermMeaning
RoutingMapping a URL path (+ HTTP method) to a server-side handler / logic.
Intent / actionWhat the request wants to do — fetch, add, update, delete (the HTTP method).
Route / route pathThe address part saying where on the server — e.g. /api/books.
HandlerThe instructions that run when a route matches; runs the business logic.
ResourceThe "thing" a route targets — a noun like users or books.
Unique keyMethod + route concatenated; what the server looks a handler up by.
Static routeFixed, constant path with no variable parameters.
Dynamic routeRoute with a variable parameter in the path.
Path parameterA dynamic value inside the path (:id); identifies which resource. Also "route parameter".
Query parameterA key=value pair after ?; metadata for filter/sort/paginate/search.
PaginationReturning a list in pages, driven by query params like page and limit.
Nested routeHierarchy of segments expressing ownership — /users/123/posts/456.
Semantic meaningA route written so it reads like a clear, human-readable statement (the goal of REST routing).
VersioningPutting /v1/, /v2/ in the route to evolve an API safely.
DeprecationMarking an old version as going away; clients get a migration window.
Catch-all routeLast-resort /* route returning a friendly "route not found".
WildcardA token (*) that matches anything; lives in the catch-all.
The whole model in one breath

A request bundles a method (the what) and a route (the where). The server joins them into a unique key, matches it against its table — static routes first, dynamic next, catch-all last — extracts any path params from the route and query params from the query string, then calls the matched handler. Version the route to evolve, deprecate to retire, and catch-all to fail gracefully.