Building Your First API with FastAPI

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:

bash
fastapi-todo/
├── pyproject.toml
├── fastapi_todo/
│ ├── main.py
│ └── models.py
└── tests/
└── test_main.py

2.2 Define Pydantic Models

fastapi_todo/models.py
1from pydantic import BaseModel, Field
2
3
4class Todo(BaseModel):
5 """Represents a Todo item."""
6 id: int
7 title: str = Field(..., min_length=1, max_length=100)
8 completed: bool = False
9
10
11class TodoCreate(BaseModel):
12 """Model for creating a new Todo."""
13 title: str = Field(..., min_length=1, max_length=100)

Explanation:

  • Todo is the full object returned to clients.
  • TodoCreate excludes id because it will be assigned by the server.
  • Validation rules (min_length, max_length) prevent bad data.

2.3 Implement CRUD Endpoints

fastapi_todo/main.py
1from typing import List
2
3from fastapi import FastAPI, HTTPException
4from .models import Todo, TodoCreate
5
6app = FastAPI()
7
8# In-memory store
9todos: List[Todo] = []
10next_id = 1
11
12
13@app.post("/todos", response_model=Todo, status_code=201)
14def create_todo(todo: TodoCreate) -> Todo:
15 """Create a new Todo item."""
16 global next_id
17 new_todo = Todo(id=next_id, title=todo.title, completed=False)
18 todos.append(new_todo)
19 next_id += 1
20 return new_todo
21
22
23@app.get("/todos", response_model=List[Todo])
24def list_todos() -> List[Todo]:
25 """List all Todo items."""
26 return todos
27
28
29@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 todo
35 raise HTTPException(status_code=404, detail="Todo not found")
36
37
38@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.title
44 return todo
45 raise HTTPException(status_code=404, detail="Todo not found")
46
47
48@app.delete("/todos/{todo_id}", status_code=204)
49def delete_todo(todo_id: int) -> None:
50 """Delete a Todo by ID."""
51 global todos
52 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.py
1from fastapi.testclient import TestClient
2from fastapi_todo.main import app
3
4client = TestClient(app)
5
6
7def test_create_todo() -> None:
8 response = client.post("/todos", json={"title": "Learn FastAPI"})
9 assert response.status_code == 201
10 data = response.json()
11 assert data["title"] == "Learn FastAPI"
12 assert data["completed"] is False
13
14
15def test_list_todos() -> None:
16 response = client.get("/todos")
17 assert response.status_code == 200
18 todos = response.json()
19 assert isinstance(todos, list)
20 assert len(todos) > 0
21
22
23def test_get_todo() -> None:
24 response = client.get("/todos/1")
25 assert response.status_code == 200
26 assert response.json()["id"] == 1
27
28
29def test_update_todo() -> None:
30 response = client.put("/todos/1", json={"title": "Learn FastAPI deeply"})
31 assert response.status_code == 200
32 assert response.json()["title"] == "Learn FastAPI deeply"
33
34
35def test_delete_todo() -> None:
36 response = client.delete("/todos/1")
37 assert response.status_code == 200
38 response = client.get("/todos")
39 data = response.json()
40 assert len(data) == 0

Explanation:

  • Tests use the same TestClient as before.
  • Each test validates the status code and response body.
  • Covers CRUD cycle: create → read → update → delete.

Run tests:

bash
poetry run pytest

2.5 Type Checking with MyPy

bash
poetry 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

bash
poetry run uvicorn fastapi_todo.main:app --reload

You can test the functionality of various routes for instance using curl:

bash
curl 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:

bash
curl -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:

bash
curl 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.

flowchart LR client("Client") server["FastAPI"] db[(Database)] client-- POST /todos -->server -- Add todo to DB --> db client-- GET /todos -->server -- Fetch all todos from DB --> db client-- GET /todos/:id -->server -- Fetch a todo based on id --> db client-- PUT /todos/:id -->server -- Update a todo in the DB --> db client-- DELETE /todos/:id -->server -- Delete a todo from DB --> db

2.8 Further Reading

Check your understanding

Test your knowledge of Building Your First API with FastAPI

Feedback