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} |
Parameter names must be valid Python identifiers, converters must be one of the supported names above, and routes use Chirp's{param} syntax rather than Flask-style <param>. Routes that differ only by parameter name, such as /users/{id} and /users/{name}, are duplicate route shapes and are rejected.
url_for() validates supplied path values against the same converter rules, so url_for("users.detail", id="alice") fails for /users/{id:int}instead of generating a URL the router cannot match.
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
pathconverters must be the final segment because they consume the rest of the URL.
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))
# Extractable dataclasses — from query (GET), form (POST), or JSON body
@app.route("/search")
def search(form: SearchForm):
return Template("search.html", q=form.q, page=form.page)
# Dependency injection via app.provide()
@app.provide()
def get_store() -> DocumentStore:
return DocumentStore()
@app.route("/documents/{id}")
def document(id: str, store: DocumentStore):
return Template("doc.html", doc=store.get(id))
Argument resolution (first match wins):
- Request — Parameter named
requestor typed asRequest - Path parameters — From URL match, with type coercion
- Extractable dataclasses — Query string (GET), form body (POST), or JSON body. Dataclass fields are populated from request data.
- Service providers — Registered via
app.provide(). When a parameter's type matches a registered factory, Chirp injects the result.
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.
Dynamic URLs in htmx Attributes
Whenchirp check <app> validates templates, it extracts hx-get, hx-post,
hx-put, hx-delete, hx-patch, action, and route-bearing macro arguments
such asconfirm_url, then verifies method + path against the route table.
Literal URLs are checked against route converter rules, so/users/alicedoes
not satisfy/users/{id:int}. Dynamic URLs (built with Kida's ~ or {{ }})
are skipped; only literal URLs are validated. Use~ or {{ var }}for path
parameters; both work at render time and are correctly treated as dynamic by
the checker.
confirm_url defaults to POST unless a companion confirm_methodis present,
which lets dialog-style component APIs participate in the same route validation
as raw htmx attributes.
Legacy component-styleaction="update-thing"values are no longer treated as
route URLs. Chirp emits a warning instead of a false route error so you can
migrate older macros to literal URLs or explicit htmx attributes over time.
The checker also validates selector-bearing HTMX attributes (hx-target, hx-select, hx-include, etc.) for obvious syntax mistakes and unknown static #idtargets.
Next Steps
- Filesystem Routing -- Discover routes from a pages/ directory
- Request & Response -- The immutable request and chainable response
- Middleware -- Intercept requests before they reach handlers
- Fragments -- Return fragments from route handlers