Skip to main content

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:
MethodDecoratorUse 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
    }

Parameter extraction

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
}

Route lookup performance

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:
RoutesFastAPI (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

Build docs developers (and LLMs) love