Route decorators
FastrAPI uses Python decorators to define routes, similar to Flask and FastAPI. Each HTTP method has its own decorator:
from fastrapi import FastrAPI
app = FastrAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.post("/items")
def create_item(data):
return {"created": data}
@app.put("/items/{item_id}")
def update_item(item_id: int, data):
return {"item_id": item_id, "updated": data}
@app.delete("/items/{item_id}")
def delete_item(item_id: int):
return {"deleted": item_id}
@app.patch("/items/{item_id}")
def patch_item(item_id: int, data):
return {"item_id": item_id, "patched": data}
@app.options("/items")
def options_items():
return {"methods": ["GET", "POST", "PUT", "DELETE", "PATCH"]}
@app.head("/items")
def head_items():
return {"status": "ok"}
Available HTTP methods
FastrAPI supports all standard HTTP methods:
| Method | Decorator | Use case |
|---|
| GET | @app.get() | Retrieve resources |
| POST | @app.post() | Create resources |
| PUT | @app.put() | Update/replace resources |
| DELETE | @app.delete() | Remove resources |
| PATCH | @app.patch() | Partial updates |
| OPTIONS | @app.options() | Get allowed methods |
| HEAD | @app.head() | Get headers only |
How decorators work
Under the hood, FastrAPI decorators parse your function and store route information in a global hashmap:
// src/app.rs
impl FastrAPI {
fn create_decorator(&self, method: &str, path: String, py: Python) -> PyResult<Py<PyAny>> {
let route_key = format!("{} {}", method, path);
let decorator = move |func: Py<PyAny>| -> PyResult<Py<PyAny>> {
// Parse function metadata (params, dependencies, etc.)
let (param_validators, response_type, dependencies, is_async, is_fast_path) =
parse_route_metadata(py, func_bound, &path);
// Store route handler
let handler = RouteHandler {
func: func.clone_ref(py),
is_async,
is_fast_path,
param_validators,
response_type,
dependencies,
// ...
};
ROUTES.pin().insert(route_key.clone(), handler);
Ok(func)
};
// Return decorator function
PyCFunction::new_closure(py, None, None, decorator).map(|f| f.into())
}
}
This happens once when the decorator is applied, not on every request. This is much faster than FastAPI’s runtime introspection.
Path parameters
Path parameters are extracted from curly braces in the route path:
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
@app.get("/items/{item_id}/reviews/{review_id}")
def get_review(item_id: int, review_id: int):
return {
"item_id": item_id,
"review_id": review_id
}
FastrAPI automatically extracts parameter names from the path at decorator time:
// src/params.rs
pub fn extract_path_param_names(path: &str) -> Vec<String> {
let mut params = Vec::new();
let mut in_param = false;
let mut current_param = String::new();
for c in path.chars() {
match c {
'{' => {
in_param = true;
current_param.clear();
}
'}' => {
if in_param && !current_param.is_empty() {
params.push(current_param.clone());
}
in_param = false;
}
_ => {
if in_param {
current_param.push(c);
}
}
}
}
params
}
Type conversion
Path parameters are automatically converted to the type specified in your function signature:
@app.get("/users/{user_id}")
def get_user(user_id: int): # Automatically converts to int
return {"user_id": user_id, "type": type(user_id).__name__}
@app.get("/posts/{slug}")
def get_post(slug: str): # Remains as string
return {"slug": slug}
Query parameters
Query parameters are extracted from the URL query string:
@app.get("/search")
def search(q: str, limit: int = 10, offset: int = 0):
return {
"query": q,
"limit": limit,
"offset": offset
}
# GET /search?q=python&limit=20&offset=10
Query parameters with default values are optional, while those without are required.
Route registration
When you call app.serve(), FastrAPI registers all decorated routes with Axum:
// src/server.rs
fn build_router(app_state: AppState) -> Router {
let mut app = Router::new();
// Iterate through all registered routes
for (route_key, handler) in ROUTES.iter() {
let parts: Vec<&str> = route_key.splitn(2, ' ').collect();
let method = parts[0]; // "GET", "POST", etc.
let path = parts[1]; // "/users/{id}"
app = register_route(app, method, path, route_key, app_state);
}
app
}
FastrAPI uses a concurrent hashmap (Papaya) for O(1) route lookups:
pub static ROUTES: Lazy<PapayaHashMap<String, RouteHandler>> =
Lazy::new(|| PapayaHashMap::with_capacity(128));
Performance comparison:
| Routes | FastAPI (regex) | FastrAPI (hashmap) |
|---|
| 10 routes | ~5μs | ~100ns |
| 100 routes | ~50μs | ~100ns |
| 1,000 routes | ~500μs | ~100ns |
| 10,000 routes | ~5ms | ~100ns |
FastrAPI’s lookup time remains constant regardless of route count.
Route handlers
Each route stores comprehensive metadata for efficient execution:
pub struct RouteHandler {
pub func: Py<PyAny>, // Python function reference
pub is_async: bool, // Async or sync function
pub is_fast_path: bool, // Skip validation if true
pub param_validators: Vec<(String, Py<PyAny>)>, // Pydantic validators
pub response_type: ResponseType, // JSON, HTML, etc.
pub needs_kwargs: bool, // Has parameters
pub path_param_names: Vec<String>, // Path params like {id}
pub query_param_names: Vec<String>, // Query params
pub body_param_names: Vec<String>, // Body params
pub dependencies: Vec<DependencyInfo>, // Dependency injection
}
Dynamic routes
You can define routes dynamically using wildcards and patterns:
@app.get("/files/{file_path:path}")
def read_file(file_path: str):
return {"file_path": file_path}
# Matches:
# /files/home/user/document.txt
# /files/images/photo.jpg
Wildcard routes should be registered after more specific routes to avoid shadowing.
WebSocket routes
FastrAPI also supports WebSocket connections:
@app.websocket("/ws")
async def websocket_endpoint(websocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Echo: {data}")
WebSocket routes are stored separately and use Axum’s WebSocket implementation.
Complete example
Here’s a full routing example showing different HTTP methods:
from fastrapi import FastrAPI
from pydantic import BaseModel
app = FastrAPI()
class Item(BaseModel):
name: str
price: float
@app.get("/")
def root():
return {"message": "Welcome to FastrAPI"}
@app.get("/items")
def list_items(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
@app.get("/items/{item_id}")
def get_item(item_id: int):
return {"item_id": item_id}
@app.post("/items")
def create_item(item: Item):
return {"name": item.name, "price": item.price}
@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
return {"item_id": item_id, "item": item}
@app.delete("/items/{item_id}")
def delete_item(item_id: int):
return {"deleted": item_id}
if __name__ == "__main__":
app.serve("127.0.0.1", 8080)
Next steps
Request handling
Learn how requests are processed
Response types
Explore different response formats