FastAPI Framework
Background reading: how Python FastAPI turns functions into HTTP endpoints
What FastAPI Does
FastAPI is a Python library that makes it easy to write web servers. You write regular Python functions with type annotations, and FastAPI handles:
- Listening for incoming HTTP requests
- Parsing request parameters and body
- Calling your function with the parsed values
- Converting your function's return value to a JSON response
- Generating error responses automatically if types don't match
A Minimal FastAPI Server
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello")
def hello():
return {"message": "Hello, world!"}
When you run this and visit http://localhost:8000/hello in a browser,
you'll see {"message": "Hello, world!"}.
FastAPI automatically converted the Python dict to JSON.
Route Decorators
The @app.get("/hello") syntax is a Python decorator —
it wraps the function and registers it with FastAPI as a route handler.
The HTTP method and path are specified in the decorator.
@app.get("/items") # responds to GET /items
@app.post("/items") # responds to POST /items
@app.put("/items/{id}") # responds to PUT /items/42 (path parameter)
@app.delete("/items/42") # responds to DELETE /items/42
Query Parameters
For GET requests, parameters come from the URL query string (?key=value).
FastAPI reads function parameter names and types to parse them automatically:
@app.get("/api/timeseries")
def api_timeseries(
t_start: float | None = None, # ?t_start=0
t_end: float | None = None, # ?t_end=10
channels: str = "", # ?channels=SEEG1,SEEG2
):
# t_start is already a float (or None if not provided)
...
FastAPI parses "0" from the URL string into 0.0 as a float.
If a required parameter is missing or the wrong type, FastAPI returns a 422 error automatically.
Request Body (POST)
For POST requests, the body is typically JSON. FastAPI uses Pydantic models to parse and validate the body automatically:
from pydantic import BaseModel
class LoadRequest(BaseModel):
path: str # required field
verbose: bool = False # optional, defaults to False
@app.post("/api/load")
def api_load(req: LoadRequest):
# req.path and req.verbose are already the right types
# If the JSON body is missing "path", FastAPI returns 422 automatically
...
The browser sends: {"path": "/data/file.edf"}
FastAPI parses it into a LoadRequest object and passes it to your function.
Routers: Splitting Routes Across Files
As a server grows, it becomes messy to put all routes in one file.
FastAPI's APIRouter lets you define routes in separate modules:
# routes/recording.py
from fastapi import APIRouter
router = APIRouter()
@router.post("/api/load")
def api_load(req: LoadRequest): ...
@router.post("/api/montage")
def api_montage(req: MontageRequest): ...
# server.py
from fastapi import FastAPI
from routes import recording, signal, metrics, notch
app = FastAPI()
app.include_router(recording.router) # registers all routes from that file
app.include_router(signal.router)
app.include_router(metrics.router)
app.include_router(notch.router)
concord-server uses exactly this pattern: four router files, assembled in server.py.
Returning Errors
FastAPI provides HTTPException for returning HTTP error responses:
from fastapi import HTTPException
@app.post("/api/load")
def api_load(req: LoadRequest):
if not Path(req.path).exists():
raise HTTPException(
status_code=404, # HTTP 404 Not Found
detail=f"File not found: {req.path}" # message in response body
)
...
The browser receives {"detail": "File not found: /path/file.edf"} with status 404.
The frontend's apiFetch function checks for non-OK status and shows this message to the user.
Serving Static Files
FastAPI can also serve static files (HTML, CSS, JS) from a directory:
from fastapi.staticfiles import StaticFiles
app.mount("/", StaticFiles(directory="static/", html=True), name="static")
With html=True, visiting / serves index.html automatically.
This is why you can open http://localhost:8000 and see the dashboard.
Important: the static file mount must be added last because it acts as a
catch-all — any path not matched by an API route is served as a static file.
If added first, it would intercept the /api/* routes before they could respond.
Uvicorn: The HTTP Server
FastAPI defines routes and handles requests, but it needs an actual network server to receive HTTP connections. In Python, this is done by an ASGI server — Montage Concord uses uvicorn.
import uvicorn
if __name__ == "__main__":
uvicorn.run("concord_server.server:app", host="0.0.0.0", port=8000)
Uvicorn handles the TCP socket, HTTP parsing, and calls into your FastAPI app for each request.
You interact with it by running python -m concord_server.server.
Why FastAPI (and not Flask or Django)?
- Type annotations: FastAPI uses Python type hints for automatic validation and documentation
- Speed: Async-first design via uvicorn/ASGI; very fast for I/O-heavy workloads
- Auto-docs: automatically generates interactive API documentation at
/docs - Pydantic integration: request body validation with clear error messages
- Minimal boilerplate: a route handler is just a Python function
http://localhost:8000/docs in your browser.
You'll see an interactive API explorer (Swagger UI) that lets you try every endpoint directly.
This is generated automatically by FastAPI from the route definitions.