A REST API to manage personal expenses, built with Python + FastAPI.
Expenses are stored in a local JSON file and persist across server restarts — no database setup required. The API includes interactive OpenAPI (Swagger UI) documentation at /docs as the optional bonus feature.
- Python 3.11+
pip install -r requirements.txtuvicorn src.main:app --reloadpytest -v# 1. Clone and enter the project
git clone https://github.com/prathamesh424/Assignment-Smart-Expense-Tracker-API.git
cd Assignment-Smart-Expense-Tracker-API
# 2. Create a virtual environment (recommended)
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # macOS / Linux
# 3. Install dependencies
pip install -r requirements.txt
# 4. Start the server
uvicorn src.main:app --reload
# 5. Run the test suite
pytest -vThe server starts at http://127.0.0.1:8000.
Interactive Swagger docs are at http://127.0.0.1:8000/docs.
| Method | Endpoint | Description | Status Codes |
|---|---|---|---|
GET |
/ |
Health check | 200 |
POST |
/expenses |
Add a new expense | 201, 422 |
GET |
/expenses |
List all expenses | 200 |
GET |
/expenses?category=Food |
Filter by category | 200 |
GET |
/expenses/total |
Total of all expenses | 200 |
GET |
/expenses/total?category=Food |
Total for a specific category | 200 |
GET |
/expenses/{id} |
Get a single expense by ID | 200, 404 |
DELETE |
/expenses/{id} |
Delete an expense | 204, 404 |
curl -X POST http://127.0.0.1:8000/expenses \
-H "Content-Type: application/json" \
-d '{
"title": "Grocery shopping",
"amount": 45.99,
"category": "Food",
"date": "2025-07-30"
}'{
"id": "d3b07384-d113-4ec2-8a1e-f2c9b6c3e7a1",
"title": "Grocery shopping",
"amount": 45.99,
"category": "Food",
"date": "2025-07-30"
}curl http://127.0.0.1:8000/expensescurl http://127.0.0.1:8000/expenses?category=FoodCategory filtering is case-insensitive — food, Food, and FOOD all match.
# Overall total
curl http://127.0.0.1:8000/expenses/total
# Total for a specific category
curl http://127.0.0.1:8000/expenses/total?category=Food{
"total": 45.99,
"category": "Food",
"count": 1
}curl -X DELETE http://127.0.0.1:8000/expenses/{id}Returns 204 No Content on success, 404 if the expense doesn't exist.
├── README.md # This file
├── AI_NOTES.md # AI usage documentation
├── requirements.txt # Pinned Python dependencies
├── src/
│ ├── __init__.py
│ ├── main.py # FastAPI app initialisation and root endpoint
│ ├── models.py # Pydantic schemas for validation and serialization
│ ├── routes.py # All /expenses endpoint definitions
│ └── storage.py # JSON file persistence layer
└── tests/
├── __init__.py
├── conftest.py # Shared pytest fixtures (isolated storage per test)
└── test_expenses.py # 22 test cases covering all endpoints
All request bodies are validated via Pydantic before reaching the endpoint logic:
| Field | Rules |
|---|---|
title |
Required, 1–200 chars, cannot be blank/whitespace |
amount |
Required, must be a positive number (> 0) |
category |
Required, 1–100 chars, cannot be blank/whitespace |
date |
Required, must be a valid date in YYYY-MM-DD |
Invalid requests return 422 Unprocessable Entity with a detailed error body showing exactly which field failed and why.
pytest -vThe test suite contains 22 tests organised into 6 groups:
| Group | Tests | What's Covered |
|---|---|---|
TestCreateExpense |
8 | Valid creation, UUID generation, validation rejections |
TestListExpenses |
5 | Empty list, populated list, category filter, case-insensitivity |
TestGetExpenseById |
2 | Found (200), not found (404) |
TestDeleteExpense |
2 | Successful deletion (204), not found (404) |
TestTotalExpenses |
4 | Empty total, overall sum, category sum, missing category |
TestRoot |
1 | Health check response |
Each test runs against an isolated temporary JSON file (via pytest tmp_path), so tests never share state or interfere with each other.
| Decision | Rationale |
|---|---|
| FastAPI | Built-in request validation (Pydantic), automatic OpenAPI docs, and minimal boilerplate |
| UUID identifiers | Simple to generate, unique without maintaining a global counter, and work well with JSON storage |
| JSON file storage | Human-readable, zero setup, satisfies the "no database" constraint, persists across server restarts |
| Case-insensitive filtering | ?category=food matches "Food" — better UX without requiring users to know exact casing |
| Injectable storage | Routes accept a storage instance via init_router(), making tests fully isolated with no shared state |
| Synchronous endpoints | File I/O is inherently blocking; async would add complexity without performance benefit |
FastAPI auto-generates an interactive Swagger UI from the route definitions and Pydantic models. Available at /docs when the server is running. Every endpoint can be tested directly from the browser — no external tools needed.