6.2. Routing
Table of Contents
Understanding Routing in Web Backends
Routing is how your backend decides which piece of code should run when a request comes in for a particular URL and HTTP method. It is like a map from URLs to functions.
In this chapter you will learn what routing is, how it works conceptually, and how typical backend frameworks handle it, with many concrete examples.
What Is Routing?
When a client (like a browser) sends a request, it includes:
- A URL, for example
/,/about,/users/123 - An HTTP method, for example
GET,POST,PUT,DELETE
Your backend needs to answer a question:
βFor this method + URL, which function should I call?β
The rules that answer this question are called routes.
A route is a combination of:
- A path pattern (like
/usersor/users/{id}) - An HTTP method (
GET,POST, etc.) - A handler function that runs when the pattern and method match
We usually describe a route like this:
| HTTP Method | Path pattern | Handler function | Description |
|---|---|---|---|
| GET | / | home() | Show homepage |
| GET | /about | about() | Show about page |
| GET | /users/{id} | get_user(id) | Show one user |
| POST | /users | create_user() | Create a new user |
| DELETE | /users/{id} | delete_user(id) | Delete a user |
The routing system finds the first route that matches the request and calls its handler.
Rule: A route is defined by method + path pattern.
The same path pattern can have multiple routes as long as the HTTP method is different.
Static vs Dynamic Routes
There are two broad kinds of route patterns you will see in all backend frameworks.
Static routes
Static routes match exactly one URL path.
Examples:
//about/contact/login
They do not contain placeholders. Only this exact path will match.
Conceptually, in Python-like pseudocode:
if method == "GET" and path == "/":
return home()
elif method == "GET" and path == "/about":
return about()Concrete example in a Flask-like style:
@app.get("/")
def home():
return "Welcome to my site"
@app.get("/about")
def about():
return "About this project"Here:
GET /callshome()GET /aboutcallsabout()GET /about/(note the trailing slash) does not match in many frameworks, unless configured
Dynamic routes
Dynamic routes have variable parts in the path. They match a family of URLs and extract values.
Examples of patterns:
/users/<id>/posts/<year>/<month>/products/<category>/<slug>
If the actual URL is /users/42 and the pattern is /users/<id>, then id will be "42" inside your handler function.
Pseudocode:
if method == "GET" and path matches "/users/<id>":
id = extract_variable(path, pattern="/users/<id>")
return get_user(id)Example:
@app.get("/users/<int:user_id>")
def get_user(user_id):
return f"User id is {user_id}"GET /users/10matches,user_idis10GET /users/abcdoes not match this pattern, because it expects an integer
Dynamic routes are very important for REST APIs, where you often work with resources identified by IDs.
Order and Specificity of Routes
Most frameworks use some form of pattern matching to decide which route to call. Two important ideas:
- More specific routes should usually come before more generic ones, especially in frameworks that match in order.
- Patterns must not conflict in a way that confuses the router.
Conflicting routes example
Imagine these routes:
@app.get("/users/profile")
def user_profile():
return "This is your profile page"
@app.get("/users/<username>")
def user_by_username(username):
return f"User: {username}"
Consider the request GET /users/profile.
- If the framework checks
/users/profilefirst, then - It exactly matches
"/users/profile", so it callsuser_profile(). - If the framework checks
/users/<username>first, then "profile"fits in<username>, so it callsuser_by_username("profile").
You can get surprising behavior if you are not careful with the order and specificity of patterns.
A safe rule to follow:
Rule: Put fixed (static) routes like /users/profile before dynamic routes like /users/<username> when your framework matches in order.
Many modern frameworks compute a "best match" automatically, but you should still design your routes to avoid ambiguity.
Routes, Methods, and Handlers
A single path can have different handlers depending on the HTTP method.
For example, for /users:
@app.get("/users")
def list_users():
...
@app.post("/users")
def create_user():
...Now:
GET /userswill calllist_users()POST /userswill callcreate_user()
Table for the same path with different methods:
| Path | Method | Typical use |
|---|---|---|
/users | GET | List all users |
/users | POST | Create a new user |
/users | PUT | Replace a user list |
/users | DELETE | Delete all users (rare) |
Remember that in a REST-specific chapter you will learn the semantic meaning of these methods. Here we focus only on their role in routing.
Key idea:
Rule: A route is unique by its (method, path) pair.
Two routes can share the same path as long as methods differ.
Route Parameters vs Query Parameters
Routing is about matching the path, not the query string.
- Path:
/users/123 - Query string:
?active=true&sort=name
The full URL might be /users/123?active=true&sort=name, but routing usually cares only about /users/123 for matching.
From the routing perspective:
/users/123?active=truematches the same route as/users/123- The query parameters
active=trueare handled separately, not in the route pattern
We will later have dedicated chapters for URL and query parameters. For now, notice the separation:
| Part | Example | Used for routing? |
|---|---|---|
| Path | /users/123 | Yes |
| Query string | ?active=true&sort=name | No |
| HTTP method | GET, POST, etc. | Yes |
| Headers, body | Various | No (for route matching) |
So a router typically matches based on:
$$
\text{route} = f(\text{HTTP method}, \text{path})
$$
and ignores query, headers, and body when deciding which handler to call.
Path Patterns and Variables
Routing systems support path parameters inside the pattern. These are placeholders which match part of the path.
Common forms:
<name>or{name}plain string<int:id>or{id:int}integer<path:subpath>or{path:path}any remaining path
The exact syntax depends on the framework. Conceptually they do the same thing.
Example: Single parameter
Pattern: /posts/<id>
Matches:
/posts/1βid = "1"/posts/abcβid = "abc"in simple string matching
Not matched:
/posts(noid)/posts/1/comments(extra part)
Example: Multiple parameters
Pattern: /posts/<year>/<month>
Requests and captures:
| Request URL | Captured variables |
|---|---|
/posts/2024/08 | year = "2024", month = "08" |
/posts/abc/xyz | year = "abc", month = "xyz" |
Example: Type-constrained parameters
Some frameworks support types in the pattern, for example:
/users/<int:id>/files/<path:filepath>
If you define:
@app.get("/users/<int:id>")
def get_user(id: int):
return f"User id: {id}"Then:
/users/10matches,idis10as an integer/users/xdoes not match at all, another route might handle it, or the framework returns 404
Rule: Type-constrained path parameters only match values of that type.
If the value cannot be converted, the route is considered not matched.
Trailing Slashes and Route Matching
Many beginners are surprised by the effect of trailing slashes.
Consider this route:
@app.get("/users")
def list_users():
...What happens with different paths:
| Path | Matches /users route? (typical) |
|---|---|
/users | Yes |
/users/ | Often no |
/users/1 | No |
Some frameworks automatically redirect to add or remove a slash, others do not.
To avoid confusion:
- Decide on a consistent style (with or without trailing slash).
- Configure or write routes to match that style.
Example of two separate routes:
@app.get("/users")
def list_users():
return "List users (no slash)"
@app.get("/users/")
def list_users_slash():
return "List users (with slash)"Now:
GET /usersmatcheslist_usersGET /users/matcheslist_users_slash
In practice you usually do not want both to exist. Keeping your routes consistent makes your API easier to use.
Route Conflicts and Ambiguity
A route conflict happens when multiple routes could match the same request. You want to avoid this.
Example of an ambiguous setup
@app.get("/files/<path:name>")
def get_file(name):
...
@app.get("/files/static")
def static_file():
...
For a request GET /files/static:
/files/staticmatches exactly/files/<path:name>also matches withname = "static"
Depending on the framework:
- It might choose the first defined route.
- It might choose the most specific one.
- It might raise an error on startup.
To reduce ambiguity:
- Favor clear, non-overlapping patterns.
- Use types or prefixes to separate concerns.
- Avoid "catch-all" routes like
/ <path:rest>unless you really need them.
Catch-all routes
A pattern like:
/<path:anything>
can match almost any path. It is usually used for special cases, such as:
- Custom 404 pages in minimal frameworks.
- Single Page Applications where unknown routes are served the same HTML file.
But it also easily creates conflicts if not handled carefully.
Rule: Use catch-all routes only when necessary, and put them after your specific routes so they do not swallow everything.
Grouping and Organizing Routes
As your application grows, you will have many routes. Good structure is important.
Common ways to organize routes
- By resource or feature
For example:
- All
/users/...routes in ausersmodule - All
/posts/...routes in apostsmodule
Pseudocode:
# users_routes.py
@app.get("/users")
def list_users(): ...
@app.post("/users")
def create_user(): ...- Route prefixes
Some frameworks let you define a prefix for a group of routes, for example /api or /api/v1.
Pseudocode:
api = APIRouter(prefix="/api")
@api.get("/users")
def list_users(): ...
Now the final path is /api/users.
- Separate routers / blueprints / controllers
Frameworks often have concepts like "router objects" or "blueprints" that you can mount under a prefix.
Imagine:
app.include_router(users_router, prefix="/users")
app.include_router(posts_router, prefix="/posts")
Then routes in users_router are under /users, in posts_router are under /posts.
This kind of organization is vital for larger backends, but the key routing idea remains the same: pattern + method β handler.
Routing and HTTP Status Codes
Routing is closely tied to how you respond with status codes, although full details of status codes are covered in a separate chapter.
Within routing, you commonly see these:
404 Not Foundwhen no route matches a request.405 Method Not Allowedwhen the path exists but not for the given method.
Example
Assume you defined only:
@app.get("/users")
def list_users():
return "Users"Requests:
| Request | Result (typical) | Why |
|---|---|---|
GET /users | 200 OK | Route matches |
POST /users | 405 Method Not Allowed | Path exists for GET but not POST |
GET /unknown | 404 Not Found | No route matches /unknown |
The routing system decides if the request is recognized (path and method combination exists). If not, the framework sends an appropriate error code.
Minimal Router Implementation (Conceptual)
To really understand routing, it helps to think about how a simple router could be implemented.
Very simple example using a dictionary
This handles only static paths, no variables:
routes = {}
def add_route(method, path, handler):
routes[(method, path)] = handler
def handle_request(method, path):
handler = routes.get((method, path))
if handler is None:
return "404 Not Found"
return handler()Usage:
def home():
return "Hello"
def about():
return "About"
add_route("GET", "/", home)
add_route("GET", "/about", about)
print(handle_request("GET", "/")) # "Hello"
print(handle_request("GET", "/about")) # "About"
print(handle_request("POST", "/about")) # "404 Not Found"With very naive dynamic paths
You could implement very basic variables like this:
dynamic_routes = [] # list of (method, pattern, handler)
def add_dynamic_route(method, pattern, handler):
dynamic_routes.append((method, pattern, handler))
def match_dynamic(method, path):
for m, pattern, handler in dynamic_routes:
if m != method:
continue
if pattern.count("/") != path.count("/"):
continue
pattern_parts = pattern.split("/")
path_parts = path.split("/")
params = {}
matched = True
for p_part, a_part in zip(pattern_parts, path_parts):
if p_part.startswith("<") and p_part.endswith(">"):
name = p_part[1:-1]
params[name] = a_part
elif p_part != a_part:
matched = False
break
if matched:
return handler, params
return None, {}Then for a request, you would:
- Try static routes dictionary.
- If no match, try dynamic patterns.
Real frameworks are much more efficient and flexible, but the main idea is still to:
- Store a list of route patterns.
- On each request, find the first pattern that fits.
- Extract parameters.
- Call the handler.
Common Routing Pitfalls for Beginners
Here are frequent mistakes newcomers make when defining routes.
1. Duplicate routes
Defining two routes with the same method and path:
@app.get("/users")
def list_users():
...
@app.get("/users")
def list_users2():
...Typically the second one overwrites the first or the framework raises an error.
Avoid: Duplicate route definitions.
2. Misusing trailing slashes
Defining /users/ but calling /users from the browser or client, and wondering why it returns 404. Or the opposite.
Fix: Pick a trailing slash style and use it consistently.
3. Overly generic routes early
Defining a route like /<anything> before more specific routes and then wondering why the specific ones never run.
Fix: Put generic patterns later and specific ones first.
4. Forgetting the method
Defining only GET /item and then trying to send a POST /item from a client.
Fix: Remember that each method and path combination is a separate route.
Summary
Routing is the part of your backend that:
- Matches HTTP method + URL path against a list of route patterns.
- Calls the corresponding handler function.
- Extracts path parameters from dynamic patterns.
- Returns 404 if no route matches, and typically 405 if the path exists but not for that method.
Key practical ideas:
- Use static routes for fixed pages like
/,/about. - Use dynamic routes with parameters for resources like
/users/{id}. - Keep paths and methods unique and unambiguous.
- Be consistent with trailing slashes.
- Group routes by feature and use prefixes to keep routes organized.
In later chapters you will build on this foundation when you learn about URL parameters, query parameters, and how frameworks like FastAPI implement routing in detail.
Views: 9
KAHIBARO