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.
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.
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
| Property | Detail |
|---|---|
| Lookup | Linear scan of the list is O(n) routes; a segment trie is O(path length) |
| Ordering | First match wins — place specific routes above broad ones |
| Param capture | :id binds one segment; wildcards bind the rest of the path |
| Ambiguity | When two patterns can fit, specificity rules (or registration order) decide the winner |
/users/:id placed above /users/new swallows the literal route — new is captured as an id. List specific routes first./users and /users/ split into different segment counts. Normalize, or register both, so one does not silently 404./* near the top matches everything below it. Keep wildcards last and as narrow as possible.GET vs POST routes a write to a read handler. Match method and path together...%2F.. fed straight into a file path is a path-traversal hole. Validate and decode params before use.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.
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.