A CRUD Task API built with FastAPI, Postgres, Docker Compose, and Redis.
The public API stayed the same:
GET /tasksGET /tasks/{task_id}POST /tasksPUT /tasks/{task_id}DELETE /tasks/{task_id}
The storage implementation changed from local SQLite code in main.py to a Postgres repository in repositories.py. The routes still expose the same URLs and request bodies, while the SQL-specific code lives behind TaskService.
Client -> FastAPI routes -> TaskService -> PostgresTaskRepository -> Postgres
Redis is included as an optional stretch service. The app pings Redis from /health, but tasks are stored only in Postgres.
Copy .env.example to .env before running locally:
cp .env.example .envThe app reads:
DATABASE_URL=postgresql://task_user:task_password@db:5432/task_api
REDIS_URL=redis://redis:6379/0
.env is gitignored. .env.example is committed so other developers know which settings are required.
Postgres runs in Docker with a named volume:
postgres_data
That volume keeps the database data after containers restart.
The table is created by init.sql:
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
done BOOLEAN NOT NULL DEFAULT FALSE
);An index is also included for the optional stretch:
CREATE INDEX IF NOT EXISTS idx_tasks_done ON tasks (done);Start the app, Postgres, and Redis together:
docker compose up --buildThe API will run at:
http://localhost:8000
Swagger docs:
http://localhost:8000/docs
Stop the stack without deleting the database volume:
docker compose downDelete the database volume only when you intentionally want to reset all data:
docker compose down -v| Method | Endpoint | Description |
|---|---|---|
| GET | / |
API information |
| GET | /health |
Health check for app, database, and Redis |
| GET | /tasks |
Get all tasks |
| GET | /tasks/{task_id} |
Get one task |
| POST | /tasks |
Create a task |
| PUT | /tasks/{task_id} |
Update a task |
| DELETE | /tasks/{task_id} |
Delete a task |
Create a task:
curl -i -X POST http://localhost:8000/tasks \
-H "Content-Type: application/json" \
-d "{\"title\":\"Persist through Docker restart\"}"PowerShell:
Invoke-WebRequest -UseBasicParsing `
-Uri "http://localhost:8000/tasks" `
-Method POST `
-ContentType "application/json" `
-Body '{"title":"Persist through Docker restart"}'List tasks:
curl http://localhost:8000/tasksUse this flow to prove persistence:
- Start the stack with
docker compose up --build. - Create a task through
POST /tasks. - Stop the stack with
docker compose down. - Start it again with
docker compose up. - Run
GET /tasksand confirm the task is still present.
The data survives because Postgres stores it in the Docker volume named postgres_data.
Open a SQL shell inside the Postgres container:
docker compose exec db psql -U task_user -d task_apiList every task:
SELECT * FROM tasks;Show only completed tasks:
SELECT * FROM tasks WHERE done = TRUE;Count all tasks:
SELECT COUNT(*) FROM tasks;Show the query plan that can use the done index:
EXPLAIN ANALYZE SELECT * FROM tasks WHERE done = TRUE;Swagger:
Previous SQLite database viewer from the earlier assignment:

