A beginner-friendly REST API built with FastAPI, SQLAlchemy, and SQLite. Covers full CRUD operations, middleware, CORS, Pydantic validation, and dependency injection.
- Full CRUD (Create, Read, Update, Delete) for Users and Tasks
- SQLite database with SQLAlchemy ORM
- Pydantic schemas for request validation and response shaping
- Middleware - request logging with timing
- CORS - cross-origin resource sharing configuration
- Dependency injection with
Depends - Auto-generated interactive API docs at
/docs
task_manager/
├── main.py ← app entry point, middleware, CORS, router registration
├── database.py ← SQLite connection, session factory, get_db dependency
├── models.py ← SQLAlchemy table definitions (User, Task)
├── schemas.py ← Pydantic validation schemas
├── crud.py ← all database operations
├── tasks.db ← SQLite database file (auto-created on first run)
└── routers/
├── __init__.py ← empty file (makes routers a Python package)
├── tasks.py ← task endpoints (full CRUD)
└── users.py ← user endpoints (full CRUD)
- Python 3.8 or higher
1. Clone or create the project folder
mkdir task_manager
cd task_manager
mkdir routers
touch routers/__init__.py2. Install dependencies
pip install fastapi uvicorn sqlalchemyuvicorn main:app --reloadThe --reload flag restarts the server automatically when you save a file — very useful during development.
You should see:
INFO: Uvicorn running on http://127.0.0.1:8000
INFO: Application startup complete.
FastAPI generates interactive documentation automatically. Once the server is running, open:
http://127.0.0.1:8000/docs
You can test every endpoint directly in the browser - no Postman needed.
All 10 endpoints are grouped neatly by resource - Users and Tasks - with colour-coded HTTP methods.
| Method | URL | Description |
|---|---|---|
POST |
/users/ |
Create a new user |
GET |
/users/ |
List all users |
GET |
/users/{id} |
Get one user (includes their tasks) |
PUT |
/users/{id} |
Update a user's name or email |
DELETE |
/users/{id} |
Delete a user |
| Method | URL | Description |
|---|---|---|
POST |
/tasks/ |
Create a new task |
GET |
/tasks/ |
List all tasks |
GET |
/tasks/{id} |
Get one task by ID |
PUT |
/tasks/{id} |
Update a task |
DELETE |
/tasks/{id} |
Delete a task |
Follow this order to see everything working together.
Step 1- Create a user
POST /users/
{
"name": "Alice",
"email": "alice@example.com"
}Step 2 - Create a task for that user
POST /tasks/
{
"title": "Learn FastAPI",
"description": "Build the task manager project",
"owner_id": 1
}Step 3 - Fetch the user and see their tasks nested inside the response
GET /users/
Response:
[
{
"name": "Alice",
"email": "alice@example.com",
"id": 1,
"tasks": [
{
"title": "Learn FastAPI",
"description": "Build the task manager project",
"id": 1,
"completed": false,
"owner_id": 1
}
]
}
]Notice how the user's tasks are nested inside the response automatically - powered by SQLAlchemy's relationship() and Pydantic's nested schemas.
Step 4 - Mark the task as completed
PUT /tasks/1
{
"completed": true
}Step 5 - Update only the user's name (other fields stay the same)
PUT /users/1
{
"name": "Alice Wanjiku"
}Step 6 - Delete the task
DELETE /tasks/1
Response:
{
"message": "Task deleted successfully"
}Every request passes through the logging middleware before reaching an endpoint. You can watch every call printed live in your terminal as you test:
Each line shows:
- The HTTP method and full URL
- The response status code (200 = success)
- How long the request took in seconds
This is great for debugging and understanding the request lifecycle.
HTTP Request
↓
Middleware (logs method, URL, response time)
↓
Router (tasks.py or users.py) - handles the endpoint
↓
crud.py - performs the database operation
↓
SQLAlchemy + SQLite (tasks.db)
↑
Response shaped by Pydantic schema → sent back to client
SQLAlchemy Models vs Pydantic Schemas
Models define the database tables. Schemas define what data comes in and goes out through the API. They are separate on purpose - you may want to hide certain database fields from the API response, or accept different fields on create vs update.
The get_db dependency
Every endpoint that needs the database includes db: Session = Depends(get_db). FastAPI calls get_db() automatically, opens a session, passes it to the endpoint, and closes it cleanly when the request is done.
The from_attributes = True config
This tells Pydantic to read data from SQLAlchemy model objects (not just plain dictionaries). Without it, converting a SQLAlchemy result to a Pydantic response would fail.
Middleware
Every request passes through the middleware before reaching an endpoint. The logging middleware in this project prints the HTTP method, URL, response status code, and how long the request took.
CORS
CORS controls which frontend origins are allowed to call the API. Without it, a browser-based frontend on a different port or domain will be blocked. In development, you can allow all origins with allow_origins=["*"]. In production, list only your real frontend URLs.
In main.py, update the origins list to match your frontend:
origins = [
"http://localhost:3000", # React
"http://localhost:5173", # Vite
]For development only, you can open it completely:
origins = ["*"] # never use this in productionGood next steps once you are comfortable with this project:
- Add password hashing to the User model with
passlib - Add JWT authentication so only logged-in users can create tasks
- Filter tasks by
owner_idso users only see their own tasks - Add a
due_datefield to Task using SQLAlchemy'sDateTimecolumn - Switch from SQLite to PostgreSQL by changing the
DATABASE_URL - Add pagination to
GET /tasks/usingskipandlimitquery parameters
MIT - free to use, modify, and share.


