PYTHON FASTAPI · LESSON 4 OF 8
Build an in-memory task API
Create, list, read, replace and delete tasks using HTTP methods.
- CRUD
- 404
- PUT
- 204
Your runnable checkpoint
Extract into a new practice folder. Each download includes complete source, requirements.txt and README.md. Stop any earlier server before running this example.
Create and activate a virtual environment as shown in lesson 1. Install with python -m pip install -r requirements.txt, then run fastapi dev main.py. Lessons 6–8 require TASK_API_KEY as explained in lesson 6.
1. Understand the concepts
CRUD means create, read, update and delete. POST /tasks creates a resource, GET retrieves it, PUT replaces its editable fields, and DELETE removes it. Use the returned id for subsequent requests instead of assuming it is always 1.
The dictionary lives inside one Python process. next_id supplies an identifier and increments after each creation. Reloading or stopping the server clears everything; multiple workers would each have separate dictionaries. This is a learning example, not persistent storage.
find_task centralizes the missing-record check. Raising HTTPException stops execution and returns a JSON error. DELETE returns 204 with an empty body. PUT requires a title and resets omitted done to False; it is replacement, not a partial update.
These tiny async routes contain no await and perform only in-memory operations on one event loop. Async does not make blocking work faster. Later synchronous SQLite operations use ordinary def endpoints so FastAPI can run them in its thread pool.
2. Build and run
These are complete files, not fragments. Save them beside each other in the extracted project folder. Open http://127.0.0.1:8000/docs to send requests.
main.py
from typing import Annotated
from fastapi import FastAPI, HTTPException, Query, Response
from pydantic import BaseModel, ConfigDict, Field
app = FastAPI()
tasks: dict[int, dict] = {}
next_id = 1
class TaskCreate(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
title: str = Field(min_length=1, max_length=120)
done: bool = False
class TaskRead(TaskCreate):
id: int
def find_task(task_id: int):
if task_id not in tasks:
raise HTTPException(status_code=404, detail="Task not found")
return tasks[task_id]
@app.post("/tasks", response_model=TaskRead, status_code=201)
async def create_task(task: TaskCreate):
global next_id
result = {"id": next_id, **task.model_dump()}
tasks[next_id] = result
next_id += 1
return result
@app.get("/tasks", response_model=list[TaskRead])
async def list_tasks(limit: Annotated[int, Query(ge=1, le=100)] = 20):
return list(tasks.values())[:limit]
@app.get("/tasks/{task_id}", response_model=TaskRead)
async def read_task(task_id: int):
return find_task(task_id)
@app.put("/tasks/{task_id}", response_model=TaskRead)
async def replace_task(task_id: int, task: TaskCreate):
find_task(task_id)
tasks[task_id] = {"id": task_id, **task.model_dump()}
return tasks[task_id]
@app.delete("/tasks/{task_id}", status_code=204)
async def delete_task(task_id: int):
find_task(task_id)
del tasks[task_id]
return Response(status_code=204)
requirements.txt
fastapi[standard]>=0.115,<1
pydantic>=2,<3
httpx>=0.27,<1
pytest>=8,<10
3. Check the result
- Create two tasks and confirm their ids differ.
- Replace the first with {"title":"Done learning","done":true}.
- Delete it: expect 204, then GET its id: expect 404.
- Restart the server: GET /tasks is empty.
4. Practice independently
Add an optional done filter to GET /tasks.
Show the explanation
Accept done: bool | None = None. Filter only when done is not None, then apply the limit. Checking if done alone would skip filtering for false.
When something goes wrong
For import errors, confirm the virtual environment is active and install requirements.txt using that environment’s Python. For 422, read the response detail and compare your input to the model. For 404, check the method, path and resource id. For 401 in later lessons, use Authorize in /docs with the same key configured on the server.
