Skip to content

Routing & Parameters

In Express you call app.get(path, handler). In FastAPI you decorate a function with @app.get(path). The decorator registers the function as the handler for that HTTP method + path combination. The function can be async def or a plain def.

TypeScript
// Express
const app = express();
app.get('/items', (req, res) => {
res.json([{ id: 1, name: 'Widget' }]);
});
app.post('/items', (req, res) => {
const body = req.body; // manually parsed
res.status(201).json(body);
});
Python
# FastAPI
from fastapi import FastAPI
app = FastAPI()
@app.get("/items")
async def list_items():
return [{"id": 1, "name": "Widget"}]
@app.post("/items", status_code=201)
async def create_item(item: dict):
return item

FastAPI supports all standard HTTP methods: @app.get, @app.post, @app.put, @app.patch, @app.delete, @app.options.

Wrap the parameter name in curly braces in the path string, then add a matching typed argument to the function. FastAPI casts the value and returns a 422 Unprocessable Entity automatically if the cast fails.

TypeScript
// Express — everything is a string, cast manually
app.get('/items/:id', (req, res) => {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) return res.status(422).json({ error: 'id must be integer' });
res.json({ id, name: 'Widget' });
});
// NestJS
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return { id, name: 'Widget' };
}
Python
# FastAPI — type hint does the cast + validation
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def get_item(item_id: int): # GET /items/abc → 422 automatically
return {"id": item_id, "name": "Widget"}
# Multiple path params
@app.get("/users/{user_id}/items/{item_id}")
async def get_user_item(user_id: int, item_id: int):
return {"user_id": user_id, "item_id": item_id}

Any function argument that is NOT in the path string is treated as a query parameter. Add a default value to make it optional.

TypeScript
// Express
app.get('/items', (req, res) => {
const skip = Number(req.query.skip) || 0;
const limit = Number(req.query.limit) || 10;
const q = req.query.q as string | undefined;
res.json({ skip, limit, q });
});
Python
# FastAPI — query params from function signature
from fastapi import FastAPI
from typing import Optional
app = FastAPI()
@app.get("/items")
async def search_items(
skip: int = 0, # GET /items?skip=5
limit: int = 10, # GET /items?limit=20
q: Optional[str] = None, # GET /items?q=widget
):
return {"skip": skip, "limit": limit, "q": q}

The in-browser runner cannot run a real HTTP server, but you can explore how FastAPI-style route matching works using a plain dict:

# Route registry — mirrors how FastAPI maps handlers
routes = {
("GET", "/items"): "list_items",
("POST", "/items"): "create_item",
("GET", "/items/{id}"): "get_item",
("PUT", "/items/{id}"): "update_item",
("DELETE", "/items/{id}"): "delete_item",
}
def match_route(method: str, path: str) -> str:
# exact match first
key = (method, path)
if key in routes:
return routes[key]
# template match (naive)
for (m, tmpl), handler in routes.items():
if m != method:
continue
parts_tmpl = tmpl.split("/")
parts_path = path.split("/")
if len(parts_tmpl) != len(parts_path):
continue
if all(t.startswith("{") or t == p for t, p in zip(parts_tmpl, parts_path)):
return handler
return "404 Not Found"
tests = [
("GET", "/items"),
("POST", "/items"),
("GET", "/items/42"),
("DELETE", "/items/99"),
("GET", "/unknown"),
]
for method, path in tests:
print(f"{method:6s} {path:20s} -> {match_route(method, path)}")
How does FastAPI know a function parameter is a path parameter vs a query parameter?
What HTTP status does FastAPI return if a client sends GET /items/abc and item_id is typed as int?
How do you make a query parameter optional in FastAPI?