Route Registration
Register routes with the@app.route()decorator:
@app.route("/")
def index():
return "Hello, World!"
@app.route("/about")
def about():
return Template("about.html")
Routes are registered during the setup phase. At freeze time, the route table compiles into an immutable trie-based structure for fast matching.
HTTP Methods
By default, routes acceptGETrequests. Specify methods explicitly:
@app.route("/users", methods=["GET"])
def list_users():
return Template("users.html", users=get_all_users())
@app.route("/users", methods=["POST"])
async def create_user(request: Request):
data = await request.json()
user = create(data)
return Response(body=b"Created").with_status(201)
@app.route("/users/{id:int}", methods=["GET", "DELETE"])
async def user(request: Request, id: int):
if request.method == "DELETE":
delete_user(id)
return Response(body=b"Deleted")
return Template("user.html", user=get_user(id))
If a request matches a path but not the method, Chirp returns 405 Method Not Allowed with an Allowheader listing the valid methods.
Path Parameters
Dynamic segments are defined with curly braces:
@app.route("/users/{id}")
def user(id: str):
return f"User: {id}"
Type Conversions
Add a type suffix to auto-convert parameters:
@app.route("/users/{id:int}")
def user(id: int): # id is an int, not a str
return get_user(id)
@app.route("/price/{amount:float}")
def price(amount: float): # amount is a float
return f"${amount:.2f}"
Supported types:
| Type | Pattern | Example |
|---|---|---|
str |
(default) any non-/chars |
/users/{name} |
int |
digits only | /users/{id:int} |
float |
digits with optional decimal | /price/{amount:float} |
path |
any chars including/ |
/files/{filepath:path} |
Catch-All Routes
Use{name:path}to match the rest of the URL:
@app.route("/files/{filepath:path}")
def serve_file(filepath: str):
return send_file(filepath) # filepath can contain slashes
Handler Signature Introspection
Chirp inspects your handler's signature to inject the right arguments:
# No arguments -- simplest case
@app.route("/")
def index():
return "Hello"
# Request only
@app.route("/search")
def search(request: Request):
q = request.query.get("q", "")
return Template("search.html", q=q)
# Path parameters only
@app.route("/users/{id:int}")
def user(id: int):
return get_user(id)
# Both
@app.route("/users/{id:int}/posts/{slug}")
def user_post(request: Request, id: int, slug: str):
return Template("post.html", post=get_post(id, slug))
If the first parameter is typed as Request, Chirp injects the request. Path parameters are matched by name.
Async Handlers
Handlers can be sync or async. Chirp handles both:
@app.route("/sync")
def sync_handler():
return "Sync"
@app.route("/async")
async def async_handler():
data = await fetch_data()
return Template("data.html", data=data)
Use async handlers when you need to awaitI/O (database queries, HTTP calls, file reads).
Error Handlers
Register error handlers by status code or exception type:
@app.error(404)
def not_found(request: Request):
return Template("errors/404.html", path=request.path)
@app.error(500)
def server_error(request: Request, error: Exception):
return Template("errors/500.html", error=str(error))
@app.error(ValidationError)
def validation_error(request: Request, error: ValidationError):
return Response(str(error)).with_status(422)
Error handlers use the same return-value system as route handlers.
Route Table Compilation
At freeze time, routes compile into a trie (prefix tree). Matching is O(path-segments), not O(total-routes). This means performance doesn't degrade as you add more routes.
The compiled route table is immutable. Under free-threading, all worker threads share it without synchronization.
Next Steps
- Request & Response -- The immutable request and chainable response
- Middleware -- Intercept requests before they reach handlers
- Fragments -- Return fragments from route handlers