A small, self-contained REST API for creating and tracking tasks, built to demonstrate a realistic multi-layer service architecture, a deterministic test suite, and a fully containerized, reproducible setup.
task-api/
├── app/
│ ├── main.py # HTTP routes / request handling
│ ├── crud.py # Data-access layer (business logic)
│ ├── models.py # SQLAlchemy ORM models
│ ├── schemas.py # Pydantic request/response validation
│ └── database.py # DB engine & session configuration
├── tests/
│ ├── conftest.py # Shared fixtures (isolated in-memory test DB)
│ └── test_tasks.py # Full CRUD + validation + error-case coverage
├── Dockerfile
├── requirements.txt
└── README.md
The project separates concerns into distinct layers — routes never talk to
the database directly, they go through crud.py, which operates on ORM
models defined independently of the API's request/response contracts
(schemas.py). This mirrors how a real production service would be
structured, rather than a single-file demo script.
- Python 3.12+ (for local, non-Docker use)
- Docker (for containerized use)
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
uvicorn app.main:app --reloadThe API will be available at http://localhost:8000.
Interactive docs (Swagger UI) are auto-generated at http://localhost:8000/docs.
docker build -t task-api .
docker run -p 8000:8000 task-apiThe API will be available at http://localhost:8000, identically to the
local run above. This is the recommended way to run the project, since it
guarantees the same Python version and dependency versions regardless of
host machine.
pip install -r requirements.txt
python -m pytest tests/ -vTests use an isolated, in-memory SQLite database per test (see
tests/conftest.py), so the suite is fully deterministic: it produces the
same pass/fail result on every run, in any order, with no shared state
between tests and no dependency on the file-based dev database.
Expected output: 11 passed.
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Liveness check |
| POST | /tasks |
Create a task |
| GET | /tasks |
List tasks (optional status_filter) |
| GET | /tasks/{id} |
Get a single task |
| PATCH | /tasks/{id} |
Partially update a task |
| DELETE | /tasks/{id} |
Delete a task |
curl -X POST http://localhost:8000/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Write documentation", "description": "For the API"}'curl "http://localhost:8000/tasks?status_filter=done"- SQLite by default — zero external dependencies for local/dev use.
The
DATABASE_URLenvironment variable can point at Postgres or another SQLAlchemy-supported database for production use, with no code changes. - Partial updates via PATCH — only fields explicitly provided in the
request body are updated, using Pydantic's
exclude_unset. - Deterministic tests — every test provisions its own fresh in-memory database via a fixture, so results never depend on execution order or leftover state from a previous run.