Skip to content

VarunKumarMup/expense-tracker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Expense Tracker API

A backend REST API for submitting and approving employee expenses, built with Java 17 + Spring Boot 4. Employees submit expenses; managers approve or reject them — with JWT authentication, role-based access, and business rules enforced at the service layer.

This is a portfolio project built to demonstrate a realistic, well-layered Spring Boot backend: authentication, authorization, persistence, validation, and tests.


Features

  • 🔐 JWT authentication — register and log in; every protected request is authenticated by a stateless token.
  • 👥 Two rolesEMPLOYEE (submits expenses) and MANAGER (approves/rejects them).
  • 🧾 Expense management — submit an expense, list your own expenses.
  • Approval workflow — expenses move PENDING → APPROVED / REJECTED, driven by a manager.
  • 🛡️ Business rules with teeth — only managers can decide; you can't approve your own expense; a settled expense can't be re-decided.
  • 🧪 Unit-tested business logic (JUnit 5 + Mockito).
  • 📦 Clean layered architecture — controller → service → repository.
  • 📖 Interactive API docs — Swagger UI (springdoc) at /swagger-ui.html, with JWT auth built into the "Authorize" button.
  • 🖥️ Built-in web frontend — a zero-dependency HTML/CSS/JS app served by Spring Boot itself at /: register, log in, submit expenses, and (as a manager) approve/reject from the browser.

Tech stack

Layer Technology
Language Java 17
Framework Spring Boot 4 (Web MVC, Data JPA, Security, Validation)
Database PostgreSQL
Auth Spring Security + JWT (jjwt), BCrypt password hashing
API docs springdoc-openapi (Swagger UI)
Frontend Vanilla HTML/CSS/JS served from Spring Boot's static/ folder
Build Maven (wrapper included)
Testing JUnit 5, Mockito, AssertJ
Boilerplate reduction Lombok

Architecture

The app follows the classic Spring layering — each layer has one job:

HTTP request
    │
    ▼
Controller   ── receives the request, validates input, returns the response
    │
    ▼
Service      ── business logic and rules (e.g. "you can't approve your own expense")
    │
    ▼
Repository   ── reads/writes the database (Spring Data JPA)
    │
    ▼
PostgreSQL

A JwtAuthenticationFilter runs before the controllers: it reads the Authorization: Bearer <token> header, validates the token, and tells Spring Security who the caller is.


Getting started

Prerequisites

  • Java 17+
  • PostgreSQL running locally (default port 5432)

1. Create the database

CREATE DATABASE expense_tracker;

2. Configure (optional)

The app runs out of the box against a local PostgreSQL using sensible defaults. To override them (recommended in any real environment), set environment variables:

Variable Default
DB_URL jdbc:postgresql://localhost:5432/expense_tracker
DB_USERNAME postgres
DB_PASSWORD postgres
JWT_SECRET a local-dev placeholder (set a real one in production)
JWT_EXPIRATION_MS 86400000 (24h)

3. Run

./mvnw spring-boot:run          # macOS/Linux
.\mvnw.cmd spring-boot:run       # Windows

The API starts on http://localhost:8080. Verify it's up:

curl http://localhost:8080/api/health      # -> OK

Web frontend

With the app running, open http://localhost:8080/ for a small built-in web UI — no separate frontend server, no build step, no framework. Spring Boot serves plain HTML/CSS/JS straight from src/main/resources/static/.

  • Register / log in — the returned JWT is kept in localStorage, so a page refresh keeps you signed in.
  • Employees submit expenses and watch their status change.
  • Managers additionally get a pending-approvals queue with Approve/Reject buttons — including friendly error messages when a business rule blocks the action (e.g. deciding your own expense).

Because the frontend is served by the same app that hosts the API, there's no CORS configuration and deploying the backend deploys the UI with it.


Interactive API docs (Swagger UI)

With the app running, open http://localhost:8080/swagger-ui.html for interactive documentation of every endpoint — you can try them straight from the browser:

  1. Call POST /api/auth/register or POST /api/auth/login and copy the token from the response.
  2. Click the Authorize 🔓 button (top right), paste the token, and confirm.
  3. Every protected endpoint now works from the UI — no curl or Postman needed.

The raw OpenAPI 3.1 spec is served at /v3/api-docs (useful for generating clients or importing into Postman).


API reference

Method Endpoint Auth Description
GET /api/health Public Health check, returns OK
POST /api/auth/register Public Create an account, returns a token
POST /api/auth/login Public Log in, returns a token
POST /api/expenses Any user Submit an expense
GET /api/expenses Any user List your own expenses
GET /api/expenses/pending Manager List all pending expenses
POST /api/expenses/{id}/approve Manager Approve an expense
POST /api/expenses/{id}/reject Manager Reject an expense

Example walkthrough

# 1. Register an employee (also returns a token)
curl -X POST http://localhost:8080/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"secret123","role":"EMPLOYEE"}'

# 2. Log in to get a token
TOKEN=$(curl -s -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"secret123"}' | jq -r .token)

# 3. Submit an expense (note the Bearer token)
curl -X POST http://localhost:8080/api/expenses \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"description":"Taxi to airport","amount":40.00,"category":"Travel"}'

# 4. List your own expenses
curl http://localhost:8080/api/expenses -H "Authorization: Bearer $TOKEN"

# 5. As a MANAGER: approve expense #1
curl -X POST http://localhost:8080/api/expenses/1/approve \
  -H "Authorization: Bearer $MANAGER_TOKEN"

Error responses

Errors return clean JSON with the appropriate status:

Status When
400 Validation failed (e.g. blank description, non-positive amount) — includes a per-field message map
401 Missing/invalid token, or wrong login credentials
403 Authenticated but not allowed (e.g. employee tries to approve; manager tries to approve their own)
409 Trying to decide an expense that's already settled

Testing

Business rules are covered by fast unit tests (no database or web server required — repositories are mocked):

./mvnw test

ExpenseServiceTest verifies the approval rules: approve/reject transitions, self-approval is forbidden, and settled expenses can't be re-decided.


Design decisions

  • DTOs, not entities, at the boundary — requests/responses use dedicated record types (CreateExpenseRequest, ExpenseResponse) so the database model never leaks to clients.
  • BigDecimal for money — never double, to avoid floating-point rounding on currency.
  • Rules live in the service — data-dependent rules ("not your own", "still pending") are in ExpenseService, where they're unit-tested; coarse role checks are declarative via @PreAuthorize.
  • Stateless JWT auth — no server-side sessions; each request proves itself with its token.
  • Passwords hashed with BCrypt — plain-text passwords are never stored.

Possible future improvements

  • Pagination on the expense lists
  • Flyway/Liquibase migrations instead of ddl-auto: update
  • Refresh tokens and token revocation
  • A Docker Compose setup for one-command Postgres + app
  • Integration tests with Testcontainers

License

This project is available under the MIT License.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors