python
43 lines · 8 steps
URL normalization middleware in FastAPI
A Starlette middleware that rewrites legacy and messy paths, then 308-redirects clients to the clean canonical URL.
Explained by
highlit
1import re
2from starlette.datastructures import URL
3from starlette.middleware.base import BaseHTTPMiddleware
4from starlette.responses import RedirectResponse
5from starlette.types import ASGIApp
6
7
8LEGACY_PREFIXES = {
9 "/api/v0/": "/api/v1/",
10 "/users/profile": "/api/v1/users/me",
11}
12
13_MULTI_SLASH = re.compile(r"/{2,}")
14
15
16class URLNormalizationMiddleware(BaseHTTPMiddleware):
17 def __init__(self, app: ASGIApp, *, redirect_slashes: bool = True) -> None:
18 super().__init__(app)
19 self.redirect_slashes = redirect_slashes
20
21 async def dispatch(self, request, call_next):
22 original = request.url.path
23 normalized = self._normalize(original)
24
25 if normalized != original:
26 target = request.url.replace(path=normalized)
27 return RedirectResponse(str(target), status_code=308)
28
29 return await call_next(request)
30
31 def _normalize(self, path: str) -> str:
32 for legacy, current in LEGACY_PREFIXES.items():
33 if path == legacy or path.startswith(legacy):
34 path = current + path[len(legacy):]
35 break
36
37 path = _MULTI_SLASH.sub("/", path)
38 path = path.lower()
39
40 if self.redirect_slashes and len(path) > 1 and path.endswith("/"):
41 path = path.rstrip("/")
42
43 return path
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Redirecting to a canonical URL keeps one authoritative path per resource instead of scattering logic across handlers.
- 2Use a 308 redirect so clients preserve the original HTTP method and body when following the new location.
- 3Centralizing path rewrites in middleware means every route benefits without touching individual endpoints.
Related explainers
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 steps
python
import secrets from django.contrib.auth import authenticate, login from django.core.cache import cache
Two-factor login with OTP in Django
two-factor-auth
one-time-passwords
caching
Intermediate
9 steps
go
package middleware import ( "net/http"
Per-plan export limits in Gin middleware
middleware
rate-limiting
authorization
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/url-normalization-middleware-in-fastapi-explained-python-88d3/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.