1. Your first API¶
Beginner · 8 min read
An API is a set of URLs that return data instead of web pages. With FastAPI each URL is a normal Python function with a decorator on top.
1.1 Hello, API¶
Save this as main.py:
from fastapi import FastAPI
app = FastAPI(title="GenAI API")
@app.get("/health")
def health():
return {"status": "ok"}
Run it:
fastapi dev main.py # development: auto-reload on save
# or
uvicorn main:app --reload # the same, with uvicorn directly
Open http://127.0.0.1:8000/health → {"status":"ok"}. Whatever you return (dict, list, Pydantic
model) is converted to JSON.
Free interactive docs
Open http://127.0.0.1:8000/docs. FastAPI lists every endpoint with a Try it out button — no
Postman needed. The raw OpenAPI spec is at /openapi.json.
1.2 Test without a server — TestClient¶
TestClient calls your app directly in Python. These notes use it to show real responses:
from fastapi.testclient import TestClient
client = TestClient(app)
r = client.get("/health")
print(r.status_code, r.json())
1.3 Path parameters¶
Part of the URL is a variable. The type hint converts and validates it:
@app.get("/documents/{doc_id}")
def get_document(doc_id: int):
return {"doc_id": doc_id, "type": type(doc_id).__name__}
print(client.get("/documents/42").json())
r = client.get("/documents/abc")
print(r.status_code, r.json()["detail"][0]["msg"])
{'doc_id': 42, 'type': 'int'}
422 Input should be a valid integer, unable to parse string as an integer
422 Unprocessable Content = "your request is the wrong shape". FastAPI sends it automatically — your function never runs.
1.4 Query parameters¶
Function arguments that are not in the path come from the query string (?key=value):
@app.get("/search")
def search(q: str, top_k: int = 5, namespace: str | None = None):
return {"q": q, "top_k": top_k, "namespace": namespace}
print(client.get("/search", params={"q": "refund policy"}).json())
print(client.get("/search?q=refund&top_k=3&namespace=support").json())
print(client.get("/search").status_code) # q is required
{'q': 'refund policy', 'top_k': 5, 'namespace': None}
{'q': 'refund', 'top_k': 3, 'namespace': 'support'}
422
Add limits with Query, the same way you use Field in Pydantic:
from typing import Annotated
from fastapi import Query
@app.get("/v2/search")
def search_v2(q: Annotated[str, Query(min_length=2)], top_k: Annotated[int, Query(ge=1, le=20)] = 5):
return {"q": q, "top_k": top_k}
print(client.get("/v2/search", params={"q": "rag", "top_k": 50}).json()["detail"][0]["msg"])
1.5 HTTP methods¶
| Method | Use it to | Decorator |
|---|---|---|
GET |
read data (search, fetch a document) | @app.get |
POST |
create or run something (chat, ingest, embed) | @app.post |
PUT / PATCH |
replace / partly update | @app.put / @app.patch |
DELETE |
remove (a document from the index) | @app.delete |
LLM calls are almost always POST: the input is large, and it isn't a simple read.
@app.post("/echo")
def echo(payload: dict):
return {"you_sent": payload}
print(client.post("/echo", json={"message": "hi"}).json())
A plain dict body accepts anything. In the next topic you replace it with a Pydantic model.
1.6 Organising routes — APIRouter¶
Big apps split routes across files and plug them in with a prefix:
from fastapi import APIRouter
docs_router = APIRouter(prefix="/v1/docs", tags=["documents"])
@docs_router.get("/")
def list_docs():
return ["policy.pdf", "faq.pdf"]
app.include_router(docs_router)
print(client.get("/v1/docs/").json())
tags group the endpoints on the /docs page.
Practice¶
- Add
GET /modelsthat returns a list of model names you support. - Add
GET /documents/{doc_id}/chunks?limit=10withlimitbetween 1 and 100.
Next: Request & response models — validated JSON in and out.