
Chapter Outline
Chapter 2: Building Your First API with FastAPI
In this chapter, we go beyond “Hello World” and build a Todo List REST API. You’ll learn:
- How to define Pydantic models for input validation.
- How to implement CRUD endpoints (
Create,Read,Update,Delete). - How to test API endpoints using Pytest + HTTPX.
- How to enforce typing with MyPy.
We’ll keep things in-memory for now (no database yet) to focus on the HTTP layer. Later chapters will swap this for a proper database backend.
2.1 Extending the Project
Project structure now looks like this:
bashfastapi-todo/├── pyproject.toml├── fastapi_todo/│ ├── main.py│ └── models.py└── tests/└── test_main.py
2.2 Define Pydantic Models
fastapi_todo/models.py1from pydantic import BaseModel, Field234class Todo(BaseModel):5 """Represents a Todo item."""6 id: int7 title: str = Field(..., min_length=1, max_length=100)8 completed: bool = False91011class TodoCreate(BaseModel):12 """Model for creating a new Todo."""13 title: str = Field(..., min_length=1, max_length=100)
Explanation:
Todois the full object returned to clients.TodoCreateexcludesidbecause it will be assigned by the server.- Validation rules (
min_length,max_length) prevent bad data.
2.3 Implement CRUD Endpoints
fastapi_todo/main.py1from typing import List23from fastapi import FastAPI, HTTPException4from .models import Todo, TodoCreate56app = FastAPI()78# In-memory store9todos: List[Todo] = []10next_id = 1111213@app.post("/todos", response_model=Todo, status_code=201)14def create_todo(todo: TodoCreate) -> Todo:15 """Create a new Todo item."""16 global next_id17 new_todo = Todo(id=next_id, title=todo.title, completed=False)18 todos.append(new_todo)19 next_id += 120 return new_todo212223@app.get("/todos", response_model=List[Todo])24def list_todos() -> List[Todo]:25 """List all Todo items."""26 return todos272829@app.get("/todos/{todo_id}", response_model=Todo)30def get_todo(todo_id: int) -> Todo:31 """Retrieve a Todo by its ID."""32 for todo in todos:33 if todo.id == todo_id:34 return todo35 raise HTTPException(status_code=404, detail="Todo not found")363738@app.put("/todos/{todo_id}", response_model=Todo)39def update_todo(todo_id: int, updated: TodoCreate) -> Todo:40 """Update a Todo’s title by ID."""41 for todo in todos:42 if todo.id == todo_id:43 todo.title = updated.title44 return todo45 raise HTTPException(status_code=404, detail="Todo not found")464748@app.delete("/todos/{todo_id}", status_code=204)49def delete_todo(todo_id: int) -> None:50 """Delete a Todo by ID."""51 global todos52 todos = [todo for todo in todos if todo.id != todo_id]
Explanation:
/todos [POST]→ create new todo./todos [GET]→ list all todos./todos/{id} [GET]→ retrieve single todo./todos/{id} [PUT]→ update title./todos/{id} [DELETE]→ remove todo.
2.4 Write Tests
tests/test_main.py1from fastapi.testclient import TestClient2from fastapi_todo.main import app34client = TestClient(app)567def test_create_todo() -> None:8 response = client.post("/todos", json={"title": "Learn FastAPI"})9 assert response.status_code == 20110 data = response.json()11 assert data["title"] == "Learn FastAPI"12 assert data["completed"] is False131415def test_list_todos() -> None:16 response = client.get("/todos")17 assert response.status_code == 20018 todos = response.json()19 assert isinstance(todos, list)20 assert len(todos) > 0212223def test_get_todo() -> None:24 response = client.get("/todos/1")25 assert response.status_code == 20026 assert response.json()["id"] == 1272829def test_update_todo() -> None:30 response = client.put("/todos/1", json={"title": "Learn FastAPI deeply"})31 assert response.status_code == 20032 assert response.json()["title"] == "Learn FastAPI deeply"333435def test_delete_todo() -> None:36 response = client.delete("/todos/1")37 assert response.status_code == 20038 response = client.get("/todos")39 data = response.json()40 assert len(data) == 0
Explanation:
- Tests use the same
TestClientas before. - Each test validates the status code and response body.
- Covers CRUD cycle: create → read → update → delete.
Run tests:
bashpoetry run pytest
2.5 Type Checking with MyPy
bashpoetry run mypy fastapi_todo
This ensures functions and models are correctly typed.
2.6 Testing the endpoints via the commandline
2.6.1 Launch the app
bashpoetry run uvicorn fastapi_todo.main:app --reload
You can test the functionality of various routes for instance using curl:
bashcurl http://127.0.0.1:8000/todos
This will initially return an empty list:
bash[]
2.6.2 Submit a new todo item
Enter the following command to add a to-do item:
bashcurl -X POST \-H "Content-Type: application/json" \-d '{"title": "Learn FastAPI deeply"}' \http://127.0.0.1:8000/todos
The server returns the complete to-do item as a response:
bash{"id":1,"title":"Learn FastAPI deeply","completed":false}
This adds a to-do item to the app's memory. To verify, run:
bashcurl http://127.0.0.1:8000/todos
Now you should see something like this as a response in your console:
bash[{"id":1,"title":"Learn FastAPI deeply","completed":false}]
If you are familiar with how REST works, you should be able to play around with all the routes and test their functionality.
2.7 API Endpoints and Flow
The following diagram depicts the information flow related to requestes made to the to-do application.
2.8 Further Reading
Check your understanding
Test your knowledge of Building Your First API with FastAPI