FastAPI Framework

Background reading: how Python FastAPI turns functions into HTTP endpoints

Prerequisite This page assumes you've read REST API & HTTP. It explains the Python side of the server.

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:

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)?

Try it! While the server is running, open 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.