Skip to content

Building Your First API — Task Manager (Part 1)


🎯 What You’ll Build

A simple Task Manager API with endpoints to:

  • ✅ Create a task
  • 📖 Read tasks (single and all)
  • ✏️ Update a task
  • ❌ Delete a task

You’ll use:

  • Pydantic v2 models for request and response validation
  • FastAPI routing and error handling
  • A Python dictionary as an in-memory database
  • Swagger UI for interactive testing

🧠 Step 1: Project Setup

Create a file called main.py and install FastAPI and Uvicorn:

pip install fastapi uvicorn

Run the server:

uvicorn main:app --reload

📦 Step 2: Define the Task Model

Use Pydantic v2 to define the structure of a task.

from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

class Task(BaseModel):
    title: str
    description: Optional[str] = None
    completed: bool = False

🗃️ Step 3: Create an In-Memory Data Store

Use a dictionary to simulate a database. Each task will have a unique ID.

tasks = {}
task_id_counter = 1

✅ Step 4: Create a Task (POST /tasks)

@app.post("/tasks", status_code=201)
def create_task(task: Task):
    global task_id_counter
    task_data = task.model_dump() # Dump the request body into a dictionary
    task_data["id"] = task_id_counter # Add an id for unique identification
    tasks[task_id_counter] = task_data # Simulate adding a record to database
    task_id_counter += 1 # Increment counter for adding another task the next time this request is made
    return task_data
  • model_dump() is the correct way to convert a Pydantic v2 model to a dictionary.
  • Assigns a unique ID and stores the task.

🧪 Example request:

{
  "title": "Buy milk",
  "description": "From the store",
  "completed": false
}

📖 Step 5: Read All Tasks (GET /tasks)

@app.get("/tasks")
def get_all_tasks():
    return list(tasks.values())

Returns a list of all tasks.


📖 Step 6: Read a Single Task by ID (GET /tasks/{task_id})

from fastapi import HTTPException

@app.get("/tasks/{task_id}")
def get_task(task_id: int):
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    return tasks[task_id]

Returns a single task or a 404 error if not found.


✏️ Step 7: Update a Task (PUT /tasks/{task_id})

@app.put("/tasks/{task_id}")
def update_task(task_id: int, updated_task: Task):
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    task_data = updated_task.model_dump()
    task_data["id"] = task_id
    tasks[task_id] = task_data
    return task_data

Replaces the entire task with new data.


❌ Step 8: Delete a Task (DELETE /tasks/{task_id})

@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    del tasks[task_id]

Deletes the task and returns a 204 No Content status.


📚 Step 9: Explore Swagger UI

FastAPI automatically generates interactive API documentation using Swagger UI.

🧭 Visit:

  • http://127.0.0.1:8000/docs → Swagger UI
  • http://127.0.0.1:8000/redoc → ReDoc (alternative view)

With Swagger UI, you can:

  • See all endpoints
  • View request and response schemas
  • Try out requests directly in the browser

This is invaluable for testing and debugging.


🧠 Recap

You now have a working CRUD API:

Method Endpoint Action
POST /tasks Create a task
GET /tasks Read all tasks
GET /tasks/{id} Read one task
PUT /tasks/{id} Update a task
DELETE /tasks/{id} Delete a task

All endpoints are:

  • Type-safe
  • Validated via Pydantic v2
  • Documented automatically

Your code should now be like this:

from fastapi import FastAPI
from fastapi import HTTPException
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

class Task(BaseModel):
    title: str
    description: Optional[str] = None
    completed: bool = False

tasks = {}
task_id_counter = 1

@app.post("/tasks", status_code=201)
def create_task(task: Task):
    global task_id_counter
    task_data = task.model_dump()
    task_data["id"] = task_id_counter
    tasks[task_id_counter] = task_data
    task_id_counter += 1
    return task_data

@app.get("/tasks")
def get_all_tasks():
    return list(tasks.values())

@app.get("/tasks/{task_id}")
def get_task(task_id: int):
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    return tasks[task_id]

@app.put("/tasks/{task_id}")
def update_task(task_id: int, updated_task: Task):
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    task_data = updated_task.model_dump()
    task_data["id"] = task_id
    tasks[task_id] = task_data
    return task_data

@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    del tasks[task_id]

🧪 Practice Challenge

Extend the API with:

  • A query parameter to filter tasks by completed=true
  • A PATCH endpoint to toggle completion status
  • A response model that hides internal fields (e.g. future owner_id)