A small REST API for tracking personal expenses, built with FastAPI. Data is
stored in a local JSON file (expenses.json) — no database required.
POST /expenses— add an expense (title, amount, category, date)GET /expenses— list all expensesGET /expenses?category=X— filter expenses by category (case-insensitive)GET /expenses?q=text— bonus: search expenses by title substringGET /expenses/summary— overall total + totals/count per categoryGET /expenses/{id}— fetch a single expenseDELETE /expenses/{id}— delete an expense
Interactive Swagger docs are auto-generated by FastAPI at /docs once the
server is running (this doubles as the optional "OpenAPI/Swagger docs" bonus).
- Python 3.11+
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txtuvicorn src.main:app --reloadThe API is now available at http://127.0.0.1:8000, and interactive docs at
http://127.0.0.1:8000/docs.
By default it stores data in expenses.json in the project root. To use a
different file, set EXPENSES_DATA_FILE before starting:
EXPENSES_DATA_FILE=my_data.json uvicorn src.main:app --reloadpytest tests/ -vTests use FastAPI's TestClient and each test gets its own temporary JSON
file (via pytest's tmp_path fixture), so running the suite never touches
expenses.json or leaves stray files behind.
curl -X POST http://127.0.0.1:8000/expenses \
-H "Content-Type: application/json" \
-d '{"title": "Coffee", "amount": 4.5, "category": "Food", "date": "2026-07-01"}'
curl "http://127.0.0.1:8000/expenses?category=Food"
curl http://127.0.0.1:8000/expenses/summary
curl -X DELETE http://127.0.0.1:8000/expenses/1src/
main.py # FastAPI app + routes (app factory pattern, see AI_NOTES.md)
models.py # Pydantic request/response models & validation
storage.py # JSON-file backed storage layer (in-memory cache + disk writes)
tests/
test_api.py # 22 tests covering CRUD, filtering, search, totals, edge cases
requirements.txt
AI_NOTES.md
- Validation:
amountmust be > 0,title/categorycan't be empty,datemust be a valid ISO date (YYYY-MM-DD) — Pydantic rejects anything else with a422. - IDs: auto-incrementing integers assigned by the server, persisted in the JSON file alongside the expenses so IDs stay unique across restarts.
/expenses/summaryis defined before/expenses/{id}inmain.py— FastAPI matches routes in order, so if{id}came first, a request to/expenses/summarywould incorrectly try to parse"summary"as an int id.- Concurrency: a
threading.Lockguards writes instorage.py. This is overkill for a single-user take-home assignment, but it's a one-line safety net against corrupting the JSON file if two requests write at once.