Skip to content
Educora
Advanced20 min27 / 42

Web services with FastAPI

Build a small REST API with FastAPI: path and query parameters, validation with Pydantic models, 201 and 404 responses, automatic docs (`/docs`), running the server on your computer and testing it with `TestClient`.

Check yourself
In this lesson you will learn
  • Create a FastAPI app and run it locally with fastapi dev or uvicorn
  • 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 /docs and TestClient

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.

Python
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}!'}
The file 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.
  1. 1
    Create a virtual environment

    In the project folder run python -m venv .venv and activate it: .venv\Scripts\activate on Windows, source .venv/bin/activate on Linux and macOS.

  2. 2
    Install FastAPI

    pip install "fastapi[standard]" — this installs FastAPI together with the uvicorn server and the fastapi command.

  3. 3
    Start the server

    fastapi dev main.py (or uvicorn main:app --reload). The server runs at http://127.0.0.1:8000 and restarts by itself when the file changes.

  4. 4
    Open the docs

    Open http://127.0.0.1:8000/docs in the browser — all the endpoints are listed there and you can try them directly with the “Try it out” button.

Terminal
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
Expected output
{"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"}]}
Because 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.

Python
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 new
tasks.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.
Python
@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.
RequestWhat it doesResponse
POST /taskscreates a new task201, or 422 (invalid body)
GET /tasks?subject=python&limit=5returns a filtered list200
GET /tasks/{task_id}returns one task200 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.

Python
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'])
Expected output
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 character
An empty title breaks the Field(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 BaseModel and Field describe and validate the JSON body; status_code=201 and HTTPException(404) give correct responses.
  • Running locally: pip install "fastapi[standard]", then fastapi dev main.py; the docs are at /docs and /redoc.
  • TestClient tests the API without a server; keep real data in a database, not in memory.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
In @app.get('/users/{user_id}') with def get_user(user_id: int, full: bool = False), what is full?