Routing¶
Route handlers can bind path values directly with Path() rather than reading them manually from req. Hypern compiles those bindings during registration, so their declaration order is independent of injection and validation markers.
HTTP Methods¶
Hypern supports all standard HTTP methods:
from hypern import Hypern, Path
app = Hypern()
@app.get("/resource")
def get_resource(req, res, ctx):
res.json({"action": "get"})
@app.post("/resource")
def create_resource(req, res, ctx):
body = req.json()
res.status(201).json({"created": body})
@app.put("/resource/:id")
def update_resource(req, res, ctx):
res.json({"action": "update"})
@app.patch("/resource/:id")
def patch_resource(req, res, ctx):
res.json({"action": "patch"})
@app.delete("/resource/:id")
def delete_resource(req, res, ctx):
res.json({"action": "delete"})
@app.options("/resource")
def options_resource(req, res, ctx):
res.header("Allow", "GET, POST, PUT, DELETE")
res.status(204).send(None)
@app.head("/resource")
def head_resource(req, res, ctx):
res.header("X-Resource-Count", "42")
res.status(200).send(None)
Route Parameters¶
Use :param syntax for dynamic path segments:
@app.get("/users/:user_id")
def get_user(res, user_id: int = Path()):
res.json({"user_id": user_id})
@app.get("/users/:user_id/posts/:post_id")
def get_user_post(
res,
user_id: int = Path(),
post_id: int = Path(),
):
res.json({"user_id": user_id, "post_id": post_id})
Path() uses the Python parameter name by default, or accepts an explicit route name such as Path("user_id"). A missing path value or failed type coercion is a normal validation error; a Path() name that is not declared by the route fails during registration.
Wildcard Routes¶
Capture remaining path segments:
@app.get("/files/*filepath")
def serve_file(req, res, ctx):
filepath = req.param("filepath")
# filepath = "path/to/file.txt" for /files/path/to/file.txt
res.json({"filepath": filepath})
Router Groups¶
Organize routes with routers and prefixes:
from hypern import Router
# API v1
api_v1 = Router(prefix="/api/v1")
@api_v1.get("/users")
def v1_users(req, res, ctx):
res.json({"version": "v1", "users": []})
@api_v1.get("/users/:id")
def v1_user(req, res, ctx):
res.json({"version": "v1"})
# API v2
api_v2 = Router(prefix="/api/v2")
@api_v2.get("/users")
def v2_users(req, res, ctx):
res.json({"version": "v2", "users": []})
# Mount routers
app.mount(api_v1)
app.mount(api_v2)
Route-Specific Middleware¶
Apply middleware to specific routes:
from hypern.middleware import CorsMiddleware
cors = CorsMiddleware(allowed_origins=["https://example.com"])
@app.get("/api/data", middleware=[cors])
def get_data(req, res, ctx):
res.json({"data": "sensitive"})
Route Metadata (OpenAPI)¶
Add metadata for API documentation using decorators:
from hypern import api_tags, api_doc
# Using decorators
@api_tags("users")
@api_doc("Get User")
@app.get("/users/:id")
def get_user(req, res, ctx):
"""Retrieve a user by their ID"""
res.json({"id": req.param("id")})
# Multiple tags
@api_tags("users", "admin")
@app.get("/users/:id/admin")
def get_user_admin(req, res, ctx):
"""Admin user retrieval endpoint"""
res.json({"id": req.param("id")})
Using Docstrings¶
OpenAPI automatically extracts documentation from docstrings:
@api_tags("users")
@app.get("/users/:id")
def get_user(req, res, ctx):
"""
Get User
Retrieve a user by their ID. Returns the user object
with all available fields.
"""
res.json({"id": req.param("id")})
Available Decorators¶
from hypern.openapi import (
tags, # Add tags to endpoint
summary, # Set summary
description, # Set description
deprecated, # Mark as deprecated
response, # Document response
operation_id, # Set custom operation ID
requires_auth, # Mark as requiring auth
)
# Complete example
@tags("users")
@summary("Create User")
@description("Create a new user account")
@response(201, "User created successfully")
@response(400, "Invalid input")
@app.post("/users")
def create_user(req, res, ctx):
"""Create a new user"""
data = req.json()
res.status(201).json({"id": 123, **data})
Async Handlers¶
Hypern supports both sync and async handlers:
import asyncio
@app.get("/sync")
def sync_handler(req, res, ctx):
res.json({"type": "sync"})
@app.get("/async")
async def async_handler(req, res, ctx):
await asyncio.sleep(0.1)
res.json({"type": "async"})
Route Priority¶
Hypern uses a high-performance radix tree router (powered by the matchit crate) for O(k) route matching, where k is the path length. Routes are matched in order of specificity:
- Exact matches (e.g.,
/users/me) - Parameterized routes (e.g.,
/users/:id) - Wildcard routes (e.g.,
/files/*filepath)
# This order matters for correct matching
@app.get("/users/me") # Matched first for /users/me
def current_user(req, res, ctx):
res.json({"user": "current"})
@app.get("/users/:id") # Matched for /users/123
def get_user(req, res, ctx):
res.json({"user_id": req.param("id")})
Route Syntax¶
Hypern uses Express.js-style routing syntax:
| Syntax | Description | Example |
|---|---|---|
:param |
Named parameter | /users/:id matches /users/123 |
*param |
Wildcard (catch-all) | /files/*filepath matches /files/a/b/c.txt |
The wildcard parameter captures everything after the prefix, including slashes.