- Create a FastAPI app and run it locally with
fastapi devoruvicorn - Declare path and query parameters with types and validate a JSON body with a Pydantic model
- Return correct status codes and check the API with
/docsandTestClient
In the previous lesson we were the client, sending requests to someone else's API. Now we switch roles and write our own server. FastAPI is a modern Python framework: it uses the type hints of your function parameters to validate incoming data, convert it to JSON and generate interactive documentation. A backend for a mobile app, a small microservice or a machine learning model served as an API — FastAPI is a popular choice for all of these.
The first app and running it locally
A server cannot run in the browser, so the code in this lesson does not run here — try it on your own computer (Python 3.10 or newer). The decorator @app.get('/path') connects a function to GET requests at that address; the dictionary the function returns is automatically turned into a JSON response.
from fastapi import FastAPI
app = FastAPI(title='Educora Demo API')
@app.get('/')
def root():
return {'message': 'Hello from FastAPI'}
@app.get('/square/{number}')
def square(number: int):
return {'number': number, 'square': number ** 2}
@app.get('/greet')
def greet(name: str, lang: str = 'en'):
greetings = {'en': 'Hello', 'az': 'Salam', 'ru': 'Привет', 'tr': 'Merhaba'}
word = greetings.get(lang, 'Hello')
return {'text': f'{word}, {name}!'}main.py. {number} is a path parameter; name and lang are not in the path, so they are query parameters, and lang has a default value, so it is optional.- 1Create a virtual environment
In the project folder run
python -m venv .venvand activate it:.venv\Scripts\activateon Windows,source .venv/bin/activateon Linux and macOS. - 2Install FastAPI
pip install "fastapi[standard]"— this installs FastAPI together with theuvicornserver and thefastapicommand. - 3Start the server
fastapi dev main.py(oruvicorn main:app --reload). The server runs athttp://127.0.0.1:8000and restarts by itself when the file changes. - 4Open the docs
Open
http://127.0.0.1:8000/docsin the browser — all the endpoints are listed there and you can try them directly with the “Try it out” button.
curl http://127.0.0.1:8000/square/12
curl "http://127.0.0.1:8000/greet?name=Aysel&lang=az"
curl http://127.0.0.1:8000/square/abc{"number":12,"square":144}
{"text":"Salam, Aysel!"}
{"detail":[{"type":"int_parsing","loc":["path","number"],"msg":"Input should be a valid integer, unable to parse string as an integer","input":"abc"}]}abc is not an integer, FastAPI does not even call the function and returns code 422 with a response that explains where the error is. In Windows PowerShell write curl.exe instead of curl.A REST API with Pydantic models
A Pydantic model describes the JSON body of a POST request: a class derived from BaseModel lists the fields and their types, and Field adds extra conditions (for example, a title of 1–100 characters). The function's return type (-> Task) validates the response too and filters out extra fields. Let us build a small API for tasks; for simplicity the data is kept in an in-memory dictionary.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
app = FastAPI(title='Educora Tasks API')
class TaskIn(BaseModel):
title: str = Field(min_length=1, max_length=100)
subject: str = 'python'
done: bool = False
class Task(TaskIn):
id: int
tasks: dict[int, Task] = {}
@app.post('/tasks', status_code=201)
def create_task(task: TaskIn) -> Task:
new = Task(id=len(tasks) + 1, **task.model_dump())
tasks[new.id] = new
return newtasks.py, part 1: models and creation. The client does not send an id — the server assigns it, which is why the input (TaskIn) and output (Task) models are separate.@app.get('/tasks')
def list_tasks(subject: str | None = None, limit: int = 10) -> list[Task]:
found = [t for t in tasks.values() if subject is None or t.subject == subject]
return found[:limit]
@app.get('/tasks/{task_id}')
def get_task(task_id: int) -> Task:
if task_id not in tasks:
raise HTTPException(status_code=404, detail='Task not found')
return tasks[task_id]tasks.py, part 2: reading. HTTPException stops the function and sends the client code 404 with {"detail": "Task not found"}. Start the server with fastapi dev tasks.py.| Request | What it does | Response |
|---|---|---|
POST /tasks | creates a new task | 201, or 422 (invalid body) |
GET /tasks?subject=python&limit=5 | returns a filtered list | 200 |
GET /tasks/{task_id} | returns one task | 200 or 404 |
Testing the API
TestClient calls the application directly without starting a real server — the requests and responses are completely real. This is ideal for pytest tests: every time you change the code, you can check all the endpoints in seconds.
from fastapi.testclient import TestClient
from tasks import app
client = TestClient(app)
r = client.post('/tasks', json={'title': 'Learn FastAPI'})
print(r.status_code, r.json())
client.post('/tasks', json={'title': 'Revise SQL', 'subject': 'sql'})
print(client.get('/tasks', params={'subject': 'python'}).json())
r = client.get('/tasks/99')
print(r.status_code, r.json())
r = client.post('/tasks', json={'title': ''})
print(r.status_code, r.json()['detail'][0]['msg'])201 {'title': 'Learn FastAPI', 'subject': 'python', 'done': False, 'id': 1}
[{'title': 'Learn FastAPI', 'subject': 'python', 'done': False, 'id': 1}]
404 {'detail': 'Task not found'}
422 String should have at least 1 characterField(min_length=1) rule — Pydantic rejects the request before it reaches the function. When subject is not sent, the default 'python' is used.Key points
app = FastAPI()and the decorators@app.get(...)and@app.post(...)bind functions to addresses; a returned dictionary becomes JSON.- A
{name}in the path is a path parameter, other arguments are query parameters; types are checked automatically (422 on error). - Pydantic's
BaseModelandFielddescribe and validate the JSON body;status_code=201andHTTPException(404)give correct responses. - Running locally:
pip install "fastapi[standard]", thenfastapi dev main.py; the docs are at/docsand/redoc. TestClienttests the API without a server; keep real data in a database, not in memory.
Check yourself
10 questions. Every correct answer earns XP.
@app.get('/users/{user_id}') with def get_user(user_id: int, full: bool = False), what is full?