A simple, containerized Fruits API built with FastAPI, SQLAlchemy, and SQLite. It exposes three endpoints to list, fetch, and add fruits, with data persisted across requests.
This repository was produced as a take-home assignment.
- FastAPI application with automatic, interactive API documentation.
- Persistent storage via SQLite (a file on disk / a mounted volume in Kubernetes).
- Tests with
pytestagainst an isolated in-memory database. - Linting & formatting with
ruff. - Containerized with a small, multi-stage Docker image.
- CI/CD with GitHub Actions: run tests → build image → publish to GitHub Container Registry (GHCR).
- Kubernetes manifests for deploying the service with a persistent volume.
| Concern | Choice | Why |
|---|---|---|
| Language / runtime | Python 3.14 | Latest stable Python (3.15 is still in alpha at time of writing). |
| Packaging | uv | Fast, reproducible dependency management and locking. |
| Web framework | FastAPI + Uvicorn | Minimal boilerplate and free OpenAPI/Swagger docs at /docs. |
| Persistence | SQLAlchemy 2.0 + SQLite | Zero external services; a single file satisfies "persistent data". |
| Tests | pytest + FastAPI TestClient | Fast, hermetic functional tests over the real ASGI app. |
Why SQLite? The assignment only requires that data persist across requests. SQLite gives us real persistence with no extra infrastructure. The database URL is configurable via the
DATABASE_URLenvironment variable, so swapping in PostgreSQL later is a one-line change.
- uv (manages Python 3.14 for you)
- Docker (optional, for containerized runs)
# Install dependencies into a virtual environment (uv manages Python 3.14)
uv sync
# Start the API (listens on http://localhost:8000)
uv run fruits-apiThen open the interactive docs at http://localhost:8000/docs.
By default the database is written to ./data/fruits.db. Override with DATABASE_URL:
DATABASE_URL="sqlite:///./data/fruits.db" uv run fruits-apiThe service exposes three fruit endpoints plus a health check.
curl http://localhost:8000/fruits[
{ "id": 1, "fruit": "apple", "color": "red" }
]curl http://localhost:8000/fruits/1{ "id": 1, "fruit": "apple", "color": "red" }Returns 404 if the fruit does not exist.
curl -X POST http://localhost:8000/fruits \
-H "Content-Type: application/json" \
-d '{"fruit": "banana", "color": "yellow"}'{ "id": 2, "fruit": "banana", "color": "yellow" }Returns 201 Created. The id is assigned by the server; fruit and color are required
non-empty strings (a missing/invalid body yields 422).
curl http://localhost:8000/health
# {"status": "ok"}uv run pytest # run the test suite
uv run ruff check . # lint
uv run ruff format . # auto-format# Build
docker build -t fruits-api .
# Run (persist the SQLite DB to a host directory)
docker run --rm -p 8000:8000 \
-e DATABASE_URL="sqlite:////data/fruits.db" \
-v "$(pwd)/data:/data" \
fruits-apiPrebuilt images are published by CI to GHCR:
docker pull ghcr.io/<owner>/<repo>:latestManifests live in k8s/ and include a Deployment, Service, and a
PersistentVolumeClaim so the SQLite database survives pod restarts.
# Update the image reference in k8s/deployment.yaml to your GHCR image, then:
kubectl apply -k k8s/
# Port-forward to try it out
kubectl port-forward svc/fruits-api 8000:80See k8s/README.md for details.
.github/workflows/ci.yml runs on every push and pull request:
- Test —
rufflint +pytest. - Build & publish — on pushes to
main(and tags), builds the Docker image and pushes it toghcr.io/<owner>/<repo>. Uses the built-inGITHUB_TOKEN; no extra secrets required.
.
├── src/fruits_api/ # application package
│ ├── main.py # FastAPI app & routes
│ ├── models.py # SQLAlchemy ORM model
│ ├── schemas.py # Pydantic request/response schemas
│ ├── database.py # engine & session management
│ ├── config.py # env-based settings
│ └── __main__.py # `python -m fruits_api` / `fruits-api` entrypoint
├── tests/ # pytest suite
├── k8s/ # Kubernetes manifests
├── Dockerfile
├── .github/workflows/ci.yml
└── pyproject.toml
Licensed under the GNU General Public License v3.0 or later. See LICENSE.