A simple REST API for managing tasks, built with FastAPI, PostgreSQL, and Docker. This project demonstrates modern Python backend development with async programming, database integration, and containerization.
- β CRUD Operations: Create, Read, Update, Delete tasks
- β Async Programming: Built with FastAPI's async/await for high performance
- β PostgreSQL Database: Persistent data storage with proper schema
- β Docker & Docker Compose: Easy setup and deployment
- β Type Safety: Pydantic models for request/response validation
- β Error Handling: Proper HTTP status codes and error messages
- β
API Documentation: Auto-generated Swagger UI at
/docs
βββββββββββββββ ββββββββββββββββ
β FastAPI βββββββββββ PostgreSQL β
β (Port 8001)β β (Port 5433) β
βββββββββββββββ ββββββββββββββββ
β
Docker Container
Tech Stack:
- Framework: FastAPI 0.104.1
- Database: PostgreSQL 15 (Alpine)
- ORM: SQLAlchemy 2.0 with async support
- Async Driver: asyncpg
- Server: Uvicorn
- Containerization: Docker & Docker Compose
.
βββ main.py # FastAPI application & endpoints
βββ models.py # SQLAlchemy ORM models & Pydantic schemas
βββ database.py # Database connection & session management
βββ tests/ # Pytest smoke tests (health check + CRUD round-trip)
βββ requirements.txt # Python dependencies
βββ requirements-dev.txt # + test/lint/type-check tooling
βββ pyproject.toml # ruff, mypy, and pytest configuration
βββ .github/workflows/ci.yml # GitHub Actions: lint, type-check, test+coverage
βββ Dockerfile # FastAPI container configuration
βββ docker-compose.yml # Multi-container setup
βββ .env.example # Example environment variables
βββ .gitignore # Git ignore rules
βββ README.md # This file
- Docker & Docker Compose
- Git (optional, for version control)
- Clone the repository (if you haven't already):
git clone https://github.com/YourUsername/Personal-Task-Manager-API.git
cd Personal-Task-Manager-API- Start the services:
docker compose up --buildYou should see:
β
Container task_manager_db Running
β
Container task_manager_app Running
β
Application startup complete
- Test the API:
curl http://localhost:8001/Expected response:
{"message":"Task Manager API is running! π"}GET /Returns: {"message":"Task Manager API is running! π"}
POST /tasks
Content-Type: application/json
{
"title": "Learn Docker",
"description": "Understand containers and Docker Compose",
"status": "in_progress"
}Response (201 Created):
{
"id": 1,
"title": "Learn Docker",
"description": "Understand containers and Docker Compose",
"status": "in_progress",
"created_at": "2026-05-21T20:30:00+00:00"
}GET /tasksResponse (200 OK):
[
{
"id": 1,
"title": "Learn Docker",
"description": "Understand containers and Docker Compose",
"status": "in_progress",
"created_at": "2026-05-21T20:30:00+00:00"
},
{
"id": 2,
"title": "Learn FastAPI",
"description": "Build REST APIs",
"status": "pending",
"created_at": "2026-05-21T20:35:00+00:00"
}
]GET /tasks/1Response (200 OK):
{
"id": 1,
"title": "Learn Docker",
"description": "Understand containers and Docker Compose",
"status": "in_progress",
"created_at": "2026-05-21T20:30:00+00:00"
}PATCH /tasks/1
Content-Type: application/json
{
"status": "completed"
}Response (200 OK):
{
"id": 1,
"title": "Learn Docker",
"description": "Understand containers and Docker Compose",
"status": "completed",
"created_at": "2026-05-21T20:30:00+00:00"
}DELETE /tasks/1Response (200 OK):
{"message":"Task 1 deleted successfully"}FastAPI auto-generates interactive API docs:
- Swagger UI: http://localhost:8001/docs
- ReDoc: http://localhost:8001/redoc
You can test all endpoints directly from these interfaces!
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description VARCHAR,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);Columns:
id: Auto-incrementing primary keytitle: Task title (required, max 255 chars)description: Detailed description (optional)status: One ofpending,in_progress,completedcreated_at: Timestamp when task was created
All endpoints use async def to handle multiple requests simultaneously without blocking. This makes the API fast and efficient.
@app.get("/tasks")
async def get_all_tasks(session: AsyncSession = Depends(get_session)):
result = await session.execute(select(TaskDB))
return result.scalars().all()Depends(get_session) automatically provides a database session to each endpoint:
async def get_task(task_id: int, session: AsyncSession = Depends(get_session)):
# session is automatically provided by FastAPI
result = await session.execute(select(TaskDB).where(TaskDB.id == task_id))Request/response validation happens automatically:
class TaskCreate(BaseModel):
title: str
description: Optional[str] = None
status: str = "pending"FastAPI validates the JSON and converts it to a Python object.
Define tables as Python classes:
class TaskDB(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True)
title = Column(String(255), nullable=False)
# ...The API returns appropriate HTTP status codes:
| Status | Meaning | Example |
|---|---|---|
| 200 | Success | GET, PATCH, DELETE worked |
| 201 | Created | POST created new task |
| 404 | Not Found | Task ID doesn't exist |
| 422 | Validation Error | Invalid request body |
| 500 | Server Error | Database connection failed |
curl -X POST http://localhost:8001/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Task 1","status":"pending"}'
curl -X POST http://localhost:8001/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Task 2","status":"in_progress"}'
curl -X POST http://localhost:8001/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Task 3","status":"completed"}'curl http://localhost:8001/tasks | jqcurl -X PATCH http://localhost:8001/tasks/2 \
-H "Content-Type: application/json" \
-d '{"status":"completed","title":"Updated Task 2"}'curl -X DELETE http://localhost:8001/tasks/3This project teaches:
- Docker: Containerization and Docker Compose
- FastAPI: Building modern REST APIs in Python
- PostgreSQL: Relational databases and SQL
- SQLAlchemy: Python ORM for database operations
- Async Programming: asyncio and async/await
- API Design: RESTful principles, status codes, error handling
Create a .env file based on .env.example:
DATABASE_URL=postgresql+asyncpg://taskuser:taskpass@db:5432/taskdb
ENVIRONMENT=development
If port 8001 or 5433 is already in use, modify docker-compose.yml:
ports:
- "8002:8000" # Change 8002 to another portMake sure Docker Desktop (or OrbStack on Mac) is running.
Check Docker logs:
docker compose logs db
docker compose logs webRebuild the image:
docker compose down
docker compose up --buildEvery push/PR to main runs three GitHub Actions jobs (see .github/workflows/ci.yml):
- Lint β ruff (config in
pyproject.toml) - Type check β mypy in strict-ish mode with
ignore_missing_imports = truefor stub-less libraries - Test & coverage β
pytestagainst a realpostgres:15-alpineservice container (same image/credentials asdocker-compose.yml), with a coverage report uploaded as a workflow artifact (coverage.xml+ HTML)
python -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
# Lint
ruff check .
# Type check
mypy main.py models.py database.py tests
# Tests + coverage (point DATABASE_URL at a disposable Postgres instance)
docker run -d --name taskdb-test -e POSTGRES_USER=taskuser -e POSTGRES_PASSWORD=taskpass \
-e POSTGRES_DB=taskdb -p 5544:5432 postgres:15-alpine
DATABASE_URL="postgresql+asyncpg://taskuser:taskpass@localhost:5544/taskdb" \
pytest --cov=main --cov=models --cov=database --cov-report=term-missing --cov-report=html
# open htmlcov/index.html for the interactive coverage report98% (main.py/models.py/database.py combined), up from an initial smoke-only baseline. On top of
the original smoke suite (tests/test_smoke.py: health check + one CRUD round-trip + a 404 check),
tests/test_tasks.py adds: validation-error coverage (missing/null/wrong-typed fields on create and
update, all returning 422), a 404 check for every not-found path (get/update/delete, including deleting
an already-deleted task), partial-update tests confirming PATCH leaves untouched fields alone, multi-row
listing/deletion scenarios, and edge cases (empty and whitespace-only titles, and the exact 255-char
column boundary for title).
Two behaviors are documented by name in the tests, since they're part of the API's current contract:
status is currently a free-form string (no enum or transition guard β any value is accepted, which keeps
the model simple for now), and a title right at the 255-character column boundary is handled explicitly
(test_create_task_title_over_column_boundary_currently_500s pins down today's behavior at that edge so
future changes to it are a deliberate decision, not a surprise).
Only main.py's if __name__ == "__main__": uvicorn.run(...) block is outside pytest's reach, since it
only runs when the file is executed directly as a script.
- Add user authentication (JWT tokens)
- Add task filtering and sorting
- Add pagination for large task lists
- Add task categories/tags
- Add due dates and reminders
- Add a status enum with transition rules
- Add input-length validation at the API layer, matching the DB column limits
- Add concurrency tests for simultaneous updates to the same task, and load testing for the listing endpoint at larger row counts
- Expand test coverage beyond the smoke suite
- Deploy to cloud (AWS, Heroku, Railway)
This project is open source and available under the MIT License.
Tejas Malhotra
- Email: tejas.malhotra.14@gmail.com
- GitHub: ApparentlyTejas
- (https://github.com/ApparentlyTejas)
Happy coding! π If you found this helpful, please star the repository!