Building APIs with FastAPI
Reviewed & published by Brayan K
FastAPI is a modern, high-performance web framework that builds typed APIs from ordinary Python type hints. You get automatic validation, interactive OpenAPI docs, async support, and dependency injection — with very little boilerplate.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- • Creating a FastAPI() app and path operations
- • Path and query parameters driven by type hints
- • Pydantic models for request and response bodies
- • Automatic OpenAPI / Swagger docs at /docs
- • Async endpoints with async def
- • Dependency injection with Depends
- • Running the server with uvicorn
🚀 1. Your First API
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello, API!"}▶️ 2. Running with uvicorn
FastAPI is an ASGI app; run it with the uvicorn server:
uvicorn main:app --reload--reload restarts the server automatically when you edit code. Then open http://127.0.0.1:8000.
🛣️ 3. Path and Query Parameters
Names in the path become path parameters; extra typed arguments become query parameters:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def get_item(item_id: int, q: str | None = None):
# /items/5?q=hello
return {"item_id": item_id, "q": q}| Parameter | Comes from |
|---|---|
| item_id | The URL path (in curly braces) |
| q | The query string (?q=...) |
Because item_id is typed int, FastAPI rejects /items/abc with a clear error.
📦 4. Request Bodies with Pydantic
Type a parameter as a Pydantic model and FastAPI parses and validates the JSON body:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
in_stock: bool = True
@app.post("/items")
def create_item(item: Item):
return {"created": item.name, "price": item.price}Send invalid JSON and FastAPI responds with a structured 422 error describing each problem.
🧪 Worked Example: A Complete Task API You Can Run
This pulls together everything from the sections above: a path parameter, a Pydantic body, a real HTTP status code, and a proper error. Read every comment before running it.
# main.py - a tiny task API, complete and runnable.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI(title="Task API")
# A Pydantic model describes the SHAPE of the JSON you are willing to accept.
class Task(BaseModel):
title: str
done: bool = False # has a default, so the client may leave it out
# A pretend database: task id -> task data.
tasks: dict[int, dict] = {1: {"title": "Write lesson", "done": True}}
@app.get("/tasks/{task_id}") # {task_id} inside the path = a path parameter
def get_task(task_id: int): # the int hint both converts AND validates it
if task_id not in tasks:
# Raising HTTPException is how you send back a real error status.
raise HTTPException(status_code=404, detail="Task not found")
return {"id": task_id, **tasks[task_id]} # ** merges the stored dict in
@app.post("/tasks", status_code=201) # 201 is the correct status for "created"
def create_task(task: Task): # typed as the model -> FastAPI parses the JSON body
new_id = max(tasks) + 1
tasks[new_id] = task.model_dump() # model_dump() turns the model back into a plain dict
return {"id": new_id, **tasks[new_id]}
if __name__ == "__main__":
# TestClient sends requests straight into the app - no port, no network.
from fastapi.testclient import TestClient
client = TestClient(app)
r = client.get("/tasks/1") # the task that already exists
print(r.status_code, r.json())
r = client.post("/tasks", json={"title": "Buy milk"}) # 'done' left out -> default False
print(r.status_code, r.json())
r = client.get("/tasks/99") # nothing there -> your 404
print(r.status_code, r.json())
r = client.get("/tasks/abc") # 'abc' is not an int -> 422
print(r.status_code, r.json()["detail"][0]["msg"])
# Expected output (a library deprecation warning may also appear above these lines,
# depending on your installed versions):
# 200 {'id': 1, 'title': 'Write lesson', 'done': True}
# 201 {'id': 2, 'title': 'Buy milk', 'done': False}
# 404 {'detail': 'Task not found'}
# 422 Input should be a valid integer, unable to parse string as an integer
# ✅ Expected output:
# 200 {'id': 1, 'title': 'Write lesson', 'done': True}
# 201 {'id': 2, 'title': 'Buy milk', 'done': False}
# 404 {'detail': 'Task not found'}
# 422 Input should be a valid integer, unable to parse string as an integerNotice you never wrote a single line of validation. The int hint produced the 422, the model produced the default done: false, and your one raise produced the 404.
📖 5. Automatic Docs
With the server running, visit these URLs in your browser:
| URL | What you get |
|---|---|
| /docs | Interactive Swagger UI |
| /redoc | Clean ReDoc documentation |
| /openapi.json | The raw OpenAPI schema |
You can try every endpoint right from /docs — no extra setup required.
⚡ 6. Async Endpoints
Use async def when an endpoint awaits I/O like a database or external API:
from fastapi import FastAPI
app = FastAPI()
@app.get("/data")
async def get_data():
# await some_async_db_call()
return {"status": "ok"}FastAPI supports both def and async def handlers; use async when you need to await something.
🔌 7. Dependency Injection
Depends() lets you share logic (auth, db sessions, common params) across endpoints:
from fastapi import FastAPI, Depends
app = FastAPI()
def pagination(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
@app.get("/users")
def list_users(page: dict = Depends(pagination)):
return {"page": page}FastAPI calls pagination, validates its parameters, and passes the result in as page.
🎯 Your Turn: Finish the Endpoints
Three blanks stand between this file and a working API: one type hint, one HTTP method, and one model annotation. Fill them in, save the file, and run it with python — the TestClient at the bottom prints the four lines you need to match.
# 🎯 YOUR TURN — replace each ___ then run this file with: python your_file.py
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class Signup(BaseModel):
email: str
age: ___ # 👉 age must be a whole number - which type is that?
@app.___("/hello/{name}") # 👉 reading data uses which method? get or post?
def hello(name: str, excited: bool = False):
# 'name' comes from the path; 'excited' has a default, so it is a query parameter.
ending = "!" if excited else "."
return {"greeting": f"Hello {name}{ending}"}
@app.post("/signup")
def signup(user: ___): # 👉 annotate it with the model so the JSON body is validated
return {"welcome": user.email, "age_next_year": user.age + 1}
client = TestClient(app)
print(client.get("/hello/Sam").json())
print(client.get("/hello/Sam?excited=true").json())
print(client.post("/signup", json={"email": "sam@example.com", "age": 29}).json())
print(client.post("/signup", json={"email": "sam@example.com", "age": "thirty"}).status_code)
# ✅ Expected output:
# {'greeting': 'Hello Sam.'}
# {'greeting': 'Hello Sam!'}
# {'welcome': 'sam@example.com', 'age_next_year': 30}
# 422🎉 Conclusion
With FastAPI you can:
✔ Build typed routes with simple decorators
✔ Validate path, query, and body data automatically
✔ Get free interactive OpenAPI docs at /docs
✔ Write async endpoints for high concurrency
✔ Share logic cleanly with dependency injection
Type hints do double duty: they document and validate your API at once.
🎯 Mini-Challenge: A Temperature API
Nothing is written for you here. Build a one-endpoint API that converts Celsius to Fahrenheit and refuses physically impossible input, then prove it works with a TestClient exactly as the worked example did.
# 🎯 MINI-CHALLENGE: a temperature API
#
# 1. Import FastAPI and HTTPException from fastapi, and TestClient from
# fastapi.testclient. Create the app.
# 2. Add GET /convert/{celsius} where celsius is typed as float.
# - Normally return {"celsius": celsius, "fahrenheit": <the conversion>}
# where the conversion is celsius * 9 / 5 + 32 rounded to 1 decimal place.
# - If celsius is below -273.15, raise HTTPException with status_code=400
# and detail="Below absolute zero".
# 3. At the bottom create a TestClient and print:
# - the .json() of GET /convert/100
# - the .status_code and .json() of GET /convert/-300, on one line
#
# ✅ Expected output:
# {'celsius': 100.0, 'fahrenheit': 212.0}
# 400 {'detail': 'Below absolute zero'}
# your code here📋 Quick Reference — FastAPI
| Syntax | What it does |
|---|---|
| app = FastAPI() | Create the application |
| @app.get('/items/{id}') | Register a GET route |
| item: ItemModel | Validated request body |
| Depends(fn) | Dependency injection |
| uvicorn main:app --reload | Run the dev server |
🎉 Great work! You've completed this lesson.
You can now build typed, documented, validated web APIs with FastAPI.
Practice quiz
How do you create a FastAPI application instance?
- app = FastAPI()
- app = fastapi.create()
- app = App()
- app = Server()
Answer: app = FastAPI(). You import FastAPI and instantiate it: app = FastAPI(); this app object holds all your routes.
Which decorator registers a handler for HTTP GET on a path?
- @get('/')
- @app.route('/')
- @app.get('/')
- @app.handle('/')
Answer: @app.get('/'). FastAPI uses one decorator per method, e.g. @app.get('/') and @app.post('/items').
In @app.get('/items/{item_id}'), how is item_id passed to the function?
- It must be parsed manually
- As a path parameter argument
- As a global variable
- Through request.body
Answer: As a path parameter argument. Names in {curly braces} become path parameters; declare a same-named function argument with a type hint.
If a function parameter has a type hint but is NOT in the path, FastAPI treats it as a what?
- Path parameter
- Header
- Cookie
- Query parameter
Answer: Query parameter. Parameters not present in the path are interpreted as query string parameters (e.g. ?limit=10).
How does FastAPI validate and parse a JSON request body?
- With a Pydantic BaseModel type-hinted parameter
- With a regular dict argument
- With request.json() only
- It does not validate bodies
Answer: With a Pydantic BaseModel type-hinted parameter. Declare a parameter typed as a Pydantic BaseModel; FastAPI parses, validates, and injects the body.
Where are the automatic interactive Swagger UI docs served by default?
- /api/docs
- /docs
- /openapi
- /swagger
Answer: /docs. FastAPI auto-generates interactive Swagger UI at /docs (and ReDoc at /redoc) from your type hints.
What keyword lets an endpoint function use await for non-blocking I/O?
- yield
- defer
- parallel
- async
Answer: async. Declaring 'async def' makes the path operation a coroutine so you can await async calls without blocking.
Which function provides dependency injection in FastAPI?
- Provide()
- Require()
- Depends()
- Inject()
Answer: Depends(). Depends() declares a dependency; FastAPI resolves it and passes the result into your endpoint.
Which ASGI server is commonly used to run a FastAPI app in development?
- gunicorn-wsgi
- uvicorn
- flask run
- node
Answer: uvicorn. FastAPI is an ASGI app; uvicorn is the standard ASGI server, e.g. 'uvicorn main:app --reload'.
What open standard does FastAPI generate to describe your API automatically?
- OpenAPI
- RAML
- GraphQL schema
- WSDL
Answer: OpenAPI. FastAPI generates an OpenAPI schema from your routes and types, which powers the /docs and /redoc UIs.
Continue this course
- Previous: Data Visualization with Matplotlib
- Next: Static Type Checking with mypy — Catch bugs before runtime with gradual typing, strict mode, generics and Protocols
- Quick reference: Python cheat sheet
- From the blog: Object-Oriented Programming in Python · Model Deployment: From Jupyter to Production