URL routing

A router is a guard that tries each pattern in order and hands the request to the first one that fits, pulling out the variable bits.

The idea

A web framework keeps a route table that maps patterns like /users/:id to handler functions. On every incoming request the router tests those patterns top-to-bottom — or descends a trie segment by segment — and the first match wins.

Patterns mix static segments (/users) with params (:id) and sometimes a trailing wildcard. When a pattern fits, the variable segments are captured and passed to the handler. Because the first fit wins, order and specificity matter: a broad pattern listed early can shadow a narrower one below it.

Pick a request, then press play to watch the router test each route in order.

How it works

Each pattern is compiled once into a list of segments. To match a request, the router splits its path into segments and walks the table: a route matches when it has the same number of segments and every static segment is equal, while :param segments capture whatever sits in that slot. The router returns the first route that fits, so route order is significant; if none fit, it returns a 404.

# Compile patterns once, then match each request top-to-bottom.
def compile(pattern):
    return [seg for seg in pattern.strip("/").split("/") if seg or pattern == "/"]

routes = [(compile(p), h) for p, h in route_table]  # ordered!

def match(path):
    parts = [s for s in path.strip("/").split("/") if s]
    for segs, handler in routes:            # first match wins
        if len(segs) != len(parts):
            continue
        params, ok = {}, True
        for seg, part in zip(segs, parts):
            if seg.startswith(":"):
                params[seg[1:]] = part       # capture the variable bit
            elif seg != part:                # static segment must equal
                ok = False
                break
        if ok:
            return handler, params
    return not_found, {}                      # nothing matched -> 404

Cost & trade-offs

PropertyDetail
LookupLinear scan of the list is O(n) routes; a segment trie is O(path length)
OrderingFirst match wins — place specific routes above broad ones
Param capture:id binds one segment; wildcards bind the rest of the path
AmbiguityWhen two patterns can fit, specificity rules (or registration order) decide the winner

Watch out for

Worked example

Route /users/42/posts through the table. The router skips / (1 segment vs 3) and /users (1 vs 3). It reaches /users/:id (2 vs 3) — still the wrong length — then tries /users/:id/posts: three segments, users equals users, :id captures 42, and posts equals posts. That route wins with id = 42, and /about below it is never reached. Had /users/:id been listed without the segment-count check, it would have shadowed this more specific route.

Check yourself

1. Two patterns can fit the same path. Which one does the router use?

2. Why should /users/new be listed above /users/:id?

Coach note: if a pick doesn't land, give it another pass — the reasoning is what sticks.