A Flask-based expense tracking API that helps users manage shared expenses within groups.
- Student 1. Hasaan Ahmed (hasaan.ahmed@student.oulu.fi)
- Student 2. Aqib Ilyas (aqib.ilyas@student.oulu.fi)
- Student 3. Muhammad Hassan Sultan (hassan.sultan@student.oulu.fi)
- Student 4. Hassan Abdullah (hassan.abdullah@student.oulu.fi)
- User management with authentication
- Group creation and membership management
- Expense tracking with participant shares
- API for creating, updating, and querying expenses
- Clone the repository:
git clone git@github.com:hasaansworld/expense-tracker.git
cd expense-tracker- Create and activate a virtual environment (optional but recommended):
python -m venv venv
source venv/bin/activate # On Windows use: venv\Scripts\activate- Install dependencies:
pip install -r requirements.txt- Initialize the database:
export FLASK_APP=expenses
flask init-dbOn Windows, use set FLASK_APP=expenses instead of export.
- (Optional) Generate test data:
export FLASK_APP=expenses
flask testgenThis will create sample data including:
- Three users (John, Jane, and Bob)
- A "Roommates" group
- A sample expense for groceries
- Sample balances between users
To start the application:
export FLASK_APP=expenses
export FLASK_DEBUG=1 # Optional: for development mode
flask runOn Windows:
set FLASK_APP=expenses
set FLASK_DEBUG=1 # Optional: for development mode
flask runThe API provides the following endpoints:
GET /api/users/- Get all usersPOST /api/users/- Create a new userGET /api/users/<user_id>- Get a specific userPUT /api/users/<user_id>- Update a userDELETE /api/users/<user_id>- Delete a user
GET /api/groups/- Get all groupsPOST /api/groups/- Create a new groupGET /api/groups/<group_id>- Get a specific groupPUT /api/groups/<group_id>- Update a groupDELETE /api/groups/<group_id>- Delete a group
GET /api/groups/<group_id>/members/- Get all members of a groupPOST /api/groups/<group_id>/members/- Add a member to a groupDELETE /api/groups/<group_id>/members/<user_id>- Remove a member from a group
GET /api/groups/<group_id>/expenses/- Get all expenses in a groupPOST /api/groups/<group_id>/expenses/- Create a new expense in a groupGET /api/expenses/<expense_id>- Get a specific expensePUT /api/expenses/<expense_id>- Update an expenseDELETE /api/expenses/<expense_id>- Delete an expense
GET /api/expenses/<expense_id>/participants/- Get all participants in an expense
The API uses API keys for authentication. When creating a user, you'll receive an API key that should be included in the X-API-Key header for authenticated requests.
Example:
curl -X POST \
http://localhost:5000/api/groups/ \
-H 'Content-Type: application/json' \
-H 'X-API-Key: your_api_key' \
-d '{"name":"New Group","description":"Group description"}'The project includes both database tests and API resources' tests.
python -m pytestTo run the db tests:
python -m pytest tests/db_test.pyThese tests verify:
- Model creation
- Relationships between models
- Data integrity
To run the API resources' tests:
python -m pytest tests/api_test.pyFor more concise output with less traceback information:
python -m pytest tests/api_test.py -v --no-header --tb=lineThe API tests verify:
- Endpoint functionality
- Authentication requirements
- Error handling
- Data validation
This API implements HATEOAS (Hypermedia as the Engine of Application State) using the Mason hypermedia format. All responses contain @controls blocks that describe available actions, methods, schemas, and navigational affordances. This makes the API self-documenting, discoverable, and REST-compliant.
Allow API clients to explore the state and possible transitions of each resource without relying on hardcoded endpoints or external documentation.
Each resource now includes a @controls object in its JSON response, showing available operations (self, update, delete, etc.) and related sub-resources.
Returns a list of groups with:
{
"groups": [
{
"id": "group-uuid",
"name": "Travel Fund",
"description": "Shared expenses for vacation",
"@controls": {
"self": { "href": "/groups/group-uuid" },
"update": { "href": "/groups/group-uuid", "method": "PUT", "encoding": "json" },
"delete": { "href": "/groups/group-uuid", "method": "DELETE" },
"members": { "href": "/groups/group-uuid/members/", "method": "GET" },
"expenses": { "href": "/groups/group-uuid/expenses/", "method": "GET" }
}
}
],
"@controls": {
"self": { "href": "/groups/" },
"create": {
"href": "/groups/",
"method": "POST",
"encoding": "json",
"schema": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"description": { "type": "string" }
}
}
}
}
}Returns members of a group:
{
"members": [
{
"id": "member-uuid",
"user_id": "user-uuid",
"role": "admin",
"joined_at": "2025-05-10T11:01:51.801722",
"user_name": "Alice",
"@controls": {
"self": { "href": "/groups/group-uuid/members/user-uuid" },
"delete": { "href": "/groups/group-uuid/members/user-uuid", "method": "DELETE" },
"user": { "href": "/users/user-uuid", "method": "GET" }
}
}
],
"@controls": {
"self": { "href": "/groups/group-uuid/members/" },
"add": {
"href": "/groups/group-uuid/members/",
"method": "POST",
"encoding": "json",
"schema": {
"type": "object",
"required": ["user_id"],
"properties": {
"user_id": { "type": "string" },
"role": { "type": "string" }
}
}
}
}
}Returns expenses in a group:
{
"expenses": [
{
"id": "expense-uuid",
"description": "Dinner",
"amount": 90.00,
"@controls": {
"self": { "href": "/expenses/expense-uuid" },
"update": { "href": "/expenses/expense-uuid", "method": "PUT" },
"delete": { "href": "/expenses/expense-uuid", "method": "DELETE" },
"participants": { "href": "/expenses/expense-uuid/participants/", "method": "GET" }
}
}
],
"@controls": {
"self": { "href": "/groups/group-uuid/expenses/" },
"create": {
"href": "/groups/group-uuid/expenses/",
"method": "POST",
"encoding": "json",
"schema": {
"type": "object",
"required": ["amount", "description"],
"properties": {
"amount": { "type": "number", "minimum": 0 },
"description": { "type": "string" },
"category": { "type": "string" },
"participants": {
"type": "array",
"items": {
"type": "object",
"required": ["user_id", "share"],
"properties": {
"user_id": { "type": "string" },
"share": { "type": "number" },
"paid": { "type": "number" }
}
}
}
}
}
}
}
}{
"participants": [
{
"user_id": "user-uuid",
"share": 30.00,
"paid": 50.00,
"balance": 20.00,
"user_name": "Alice",
"@controls": {
"user": { "href": "/users/user-uuid", "method": "GET" }
}
}
],
"@controls": {
"self": { "href": "/expenses/expense-uuid/participants/" },
"add": {
"href": "/expenses/expense-uuid/participants/",
"method": "POST",
"encoding": "json",
"schema": {
"type": "object",
"required": ["user_id", "share"],
"properties": {
"user_id": { "type": "string" },
"share": { "type": "number", "minimum": 0 },
"paid": { "type": "number", "minimum": 0 }
}
}
}
}
}{
"users": [
{
"id": "user-uuid",
"name": "Alice",
"email": "alice@example.com",
"@controls": {
"self": { "href": "/users/user-uuid" },
"update": { "href": "/users/user-uuid", "method": "PUT", "encoding": "json" }
}
}
],
"@controls": {
"self": { "href": "/users/" },
"create": {
"href": "/users/",
"method": "POST",
"encoding": "json",
"schema": {
"type": "object",
"required": ["name", "email", "password_hash"],
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"password_hash": { "type": "string" }
}
}
}
}
}{
"id": "user-uuid",
"name": "Alice",
"email": "alice@example.com",
"@controls": {
"self": { "href": "/users/user-uuid" },
"update": {
"href": "/users/user-uuid",
"method": "PUT",
"encoding": "json",
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" }
}
}
}
}
}Hypermedia controls are generated using a reusable MasonBuilder utility class and build_<resource>_controls() helpers in each resource. These controls include:
self: canonical link to the resourceupdate,delete: if applicableschema: JSON schema describing required fields and typesmethodandencoding: for correct HTTP method and content type declaration
The client folder contains client-side application for Expense Tracker app, built with React.js and Vite. The application serves as a showcase for a subset of the REST api functionality.
- React.js: Frontend library for building user interfaces
- Vite.js: Next-generation frontend tooling for faster development
- Tailwind CSS: Utility-first CSS framework for rapid UI development
- React Router: For handling application routing
- Axios: Promise-based HTTP client for API requests
Follow these steps to set up and run the client application locally:
- Node.js (v16.0.0 or higher)
- npm (v7.0.0 or higher)
Navigate to the client directory:
cd clientInstall dependencies:
npm iSet up environment variables:
# Copy the example environment file
cp .env.example .env
# Open .env and update the API base URL
# Example: VITE_API_BASE_URL=http://localhost:3000/apiStart the development server:
npm run devCopy the URL shown in your terminal (usually http://localhost:5173/) and paste it into your browser to view the application.
To run ESLint for linting the project use:
npm run lintHere's a breakdown of the main parts:
This folder contains static files like images, icons, or other assets that don't need to be processed by the app. These are served directly to the browser.
The heart of the frontend code. Everything inside here is part of the actual React app.
Reusable UI building blocks of the app, such as:
Card.jsx– A styled container for content.Header.jsx– Used for the top navigation and branding.HypermediaForm.jsx– A dynamic form, driven by schemas from hypermedia controls in API responses.Input.jsx– A reusable input component for forms.ProtectedRoute.jsx– A wrapper that restricts access to certain pages unless the user is authenticated.RootLayout.jsx– The main layout wrapper that may include shared elements like headers or sidebars.
Each file in this folder represents a full page or route in the application. Examples include:
Home.jsx– The landing or dashboard page.Signup.jsx– A page to register new users.CreateGroup.jsx,AddMembers.jsx,CreateExpense.jsx, etc. – Pages for managing groups and expenses.
The main component that ties the entire app together. It usually includes the router and wraps all pages.
The entry point where the React app is mounted to the HTML DOM.
Global styles for the entire application.
A JavaScript utility file – likely contains logic to support HypermediaForm.jsx.
.env/.env.example– Used to store environment-specific variables like API URLs.vite.config.js– Configuration for Vite (the build tool).eslint.config.js– Linting rules for consistent code style.index.html– The HTML shell that React injects the app into.package.json– Lists all dependencies and scripts to run the app.
This structure is designed to keep the code organized and scalable as the project grows.
- User authentication (signup only)
- Create groups
- Add members to groups
- Create expenses
- Delete groups
This project is for educational purposes as part of the PWP course at the University of Oulu.