A full-stack Java/Spring Boot + React portfolio project that models a clinic appointment and billing workflow. The project is designed for Java/React job applications: it demonstrates API design, layered Spring Boot architecture, JWT authentication, role-based access control, Flyway migrations, Docker deployment, and a TypeScript React frontend.
This is not intended to be a real clinical or medical billing product.
- Java 21 and Spring Boot 3 REST API design.
- Layered backend structure with controllers, DTOs, services, repositories, entities, and centralized exception handling.
- Spring Security with BCrypt password hashing, JWT access tokens, and role-based access control.
- PostgreSQL persistence with Flyway-managed schema migrations.
- Business rules for invoices, payments, status transitions, and overpayment prevention.
- React + Vite + TypeScript frontend with protected routes and role-aware navigation.
- Production-style Docker Compose deployment with PostgreSQL, Spring Boot, and Nginx.
- Swagger/OpenAPI documentation and CI-ready build/test workflows.
Screenshots should be added before sharing this repository publicly:
- Dashboard:
docs/screenshots/dashboard.png - Login:
docs/screenshots/login.png - Invoices:
docs/screenshots/invoices.png - Invoice detail:
docs/screenshots/invoice-detail.png - Admin users:
docs/screenshots/users.png
The directory is present at docs/screenshots/ with a .gitkeep placeholder.
The application seeds one admin user when the backend starts and no admin with that email exists.
Local default:
Email: admin@clinic.local
Password: admin12345
Role: ADMIN
Production example defaults from deploy/prod/.env.example:
Email: admin@clinic.local
Password: change_me_admin_password
Role: ADMIN
Change APP_ADMIN_EMAIL and APP_ADMIN_PASSWORD before using a persistent environment. The bootstrap password is only used when the admin user is first created.
- JWT login and
/api/auth/meprofile endpoint. - ADMIN-only user management.
- Role-aware React navigation.
- Patient and appointment workflows for
ADMIN,RECEPTIONIST, andDOCTOR. - Service catalogue, invoices, and payments for
ADMINandBILLING. - Invoice statuses:
DRAFT,ISSUED,PARTIALLY_PAID,PAID,CANCELLED. - Automatic invoice subtotal, amount paid, and balance due calculations.
- Swagger/OpenAPI docs.
- Dockerized local database and production stack.
- Backend: Java 21, Spring Boot 3, Spring Security, Spring Web, Spring Data JPA, Validation, Flyway, Actuator, Springdoc OpenAPI
- Frontend: React, TypeScript, Vite, React Router, Axios
- Database: PostgreSQL
- Local development: PostgreSQL in Docker, backend and frontend on the host
- Production: PostgreSQL, Spring Boot API, and Nginx-served React build in Docker Compose
Browser
-> React + Vite SPA
-> Axios API client with Bearer token
-> Spring Boot REST API
-> Spring Security JWT/RBAC
-> Service layer business rules
-> Spring Data JPA repositories
-> PostgreSQL
Production deployment:
Browser
-> Nginx frontend container
-> static React files
-> /api proxy
-> Spring Boot backend container
-> PostgreSQL container
More detail: docs/ARCHITECTURE.md
erDiagram
PATIENTS ||--o{ APPOINTMENTS : schedules
PATIENTS ||--o{ INVOICES : receives
INVOICES ||--o{ INVOICE_ITEMS : contains
INVOICES ||--o{ PAYMENTS : records
SERVICE_ITEMS ||--o{ INVOICE_ITEMS : prices
USERS ||--o{ ROLES : has
PATIENTS {
uuid id PK
varchar first_name
varchar last_name
varchar email UK
varchar phone
date date_of_birth
timestamptz created_at
timestamptz updated_at
}
APPOINTMENTS {
uuid id PK
uuid patient_id FK
timestamptz scheduled_at
varchar reason
varchar status
timestamptz created_at
timestamptz updated_at
}
SERVICE_ITEMS {
uuid id PK
varchar name UK
varchar description
numeric default_price
boolean active
timestamptz created_at
timestamptz updated_at
}
INVOICES {
uuid id PK
uuid patient_id FK
varchar invoice_number UK
varchar status
numeric subtotal
numeric amount_paid
numeric balance_due
timestamptz issued_at
timestamptz created_at
timestamptz updated_at
}
INVOICE_ITEMS {
uuid id PK
uuid invoice_id FK
uuid service_item_id FK
varchar description
integer quantity
numeric unit_price
numeric line_total
timestamptz created_at
}
PAYMENTS {
uuid id PK
uuid invoice_id FK
numeric amount
varchar method
varchar reference
timestamptz paid_at
timestamptz created_at
}
USERS {
uuid id PK
varchar email UK
varchar full_name
varchar password_hash
boolean enabled
timestamptz created_at
timestamptz updated_at
}
ROLES {
uuid user_id FK
varchar role
}
clinic-system/
backend/
src/main/java/com/example/clinic/
src/main/resources/db/migration/
pom.xml
Dockerfile
.env.example
frontend/
src/
package.json
vite.config.js
Dockerfile
nginx.conf
.env.example
deploy/
local/docker-compose.yml
prod/docker-compose.yml
prod/.env.example
docs/
ARCHITECTURE.md
screenshots/.gitkeep
.github/workflows/ci.yml
README.md
- Docker and Docker Compose
- Java 21 for local backend development
- Node.js 22 recommended for local frontend development
curlorwgetif you use the includedbackend/mvnwMaven launcher
From the repository root, run these in separate terminals because the backend and frontend commands stay attached:
docker compose -f deploy/local/docker-compose.yml up -d
(cd backend && cp -n .env.example .env && ./mvnw spring-boot:run -Dspring-boot.run.profiles=local)
(cd frontend && cp -n .env.example .env && npm ci && npm run dev)Then open:
- Frontend: http://localhost:5173
- Swagger UI: http://localhost:8080/swagger-ui.html
- API ping: http://localhost:8080/api/health/ping
Login locally with admin@clinic.local / admin12345.
Start PostgreSQL only:
cd clinic-system
docker compose -f deploy/local/docker-compose.yml up -dRun the backend:
cd backend
cp .env.example .env
./mvnw spring-boot:run -Dspring-boot.run.profiles=localRun the frontend:
cd frontend
cp .env.example .env
npm ci
npm run devLocal URLs:
- Frontend: http://localhost:5173
- API ping: http://localhost:8080/api/health/ping
- Actuator health: http://localhost:8080/actuator/health
- Swagger UI: http://localhost:8080/swagger-ui.html
- Admin users page: http://localhost:5173/users
Create a production env file:
cd clinic-system
cp deploy/prod/.env.example deploy/prod/.envEdit deploy/prod/.env and set strong database, JWT, and admin bootstrap secrets. At minimum, change POSTGRES_PASSWORD, DB_PASSWORD, APP_JWT_SECRET, and APP_ADMIN_PASSWORD.
Build and start the stack:
docker compose -f deploy/prod/docker-compose.yml --env-file deploy/prod/.env up -d --buildProduction-style URLs:
- Frontend and API through Nginx: http://localhost
- Swagger UI through Nginx: http://localhost/swagger-ui.html
PostgreSQL is only available inside the Docker network in production Compose.
Backend:
SPRING_PROFILES_ACTIVE:localorprodDB_URL: PostgreSQL JDBC URLDB_USERNAME: database usernameDB_PASSWORD: database passwordFRONTEND_ORIGIN: allowed CORS origin, comma-separated if neededSERVER_PORT: optional backend port, defaults to8080APP_JWT_SECRET: HMAC signing secret, at least 32 bytesAPP_JWT_ISSUER: JWT issuer, defaults toclinic-systemAPP_JWT_ACCESS_TOKEN_MINUTES: access token lifetime in minutesAPP_ADMIN_EMAIL: bootstrap admin emailAPP_ADMIN_PASSWORD: bootstrap admin password used only when the admin user does not already exist
Frontend:
VITE_API_BASE_URL: API base URL. Usehttp://localhost:8080/apilocally and/apiin production.
Local PostgreSQL:
POSTGRES_PASSWORD: optional, defaults toclinic_passwordindeploy/local/docker-compose.yml
Public endpoints:
GET /api/health/pingGET /actuator/healthGET /swagger-ui.htmlGET /v3/api-docsPOST /api/auth/login
Authenticated endpoints:
GET /api/auth/meGET /api/patientsPOST /api/patientsGET /api/appointmentsPOST /api/appointmentsGET /api/usersPOST /api/usersGET /api/servicesPOST /api/servicesGET /api/invoicesGET /api/invoices/{id}POST /api/invoicesPOST /api/invoices/{id}/itemsPOST /api/invoices/{id}/issuePOST /api/invoices/{id}/payments
Role access:
ADMIN: full application access, including user managementRECEPTIONIST: patients and appointmentsDOCTOR: patients and appointmentsBILLING: service catalogue, invoices, and payments
Swagger documents the API and includes a Bearer JWT security scheme. Use POST /api/auth/login, copy the returned token, and authorize in Swagger with Bearer <token>.
Flyway runs automatically when the Spring Boot application starts.
backend/src/main/resources/db/migration/V1__init.sql
backend/src/main/resources/db/migration/V2__billing.sql
backend/src/main/resources/db/migration/V3__auth.sql
The migrations create patients, appointments, service_items, invoices, invoice_items, payments, users, and roles tables with UUID primary keys, constraints, and useful indexes.
Backend tests:
cd backend
./mvnw testFrontend checks:
cd frontend
npm ci
npm run lint
npm run buildDocker Compose config validation:
docker compose -f deploy/local/docker-compose.yml config
docker compose -f deploy/prod/docker-compose.yml --env-file deploy/prod/.env.example configGitHub Actions workflow: .github/workflows/ci.yml
It runs on push and pull_request:
- Backend Maven tests on Java 21
- Frontend
npm ci, lint, and build on Node 22 - Backend and frontend Docker image builds
Build backend:
cd backend
./mvnw clean packageBuild frontend:
cd frontend
npm ci
npm run buildLogin and call a protected endpoint against production-style Compose:
TOKEN=$(curl -fsS -X POST http://localhost/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@clinic.local","password":"change_me_admin_password"}' \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["accessToken"])')
curl -H "Authorization: Bearer $TOKEN" http://localhost/api/auth/me
curl -H "Authorization: Bearer $TOKEN" http://localhost/api/patientsFor local host-run backend commands, use http://localhost:8080 and the local admin password from backend/.env.
Stop local database:
docker compose -f deploy/local/docker-compose.yml downStop production-style stack:
docker compose -f deploy/prod/docker-compose.yml --env-file deploy/prod/.env downRemove database volumes when you intentionally want a clean database:
docker compose -f deploy/local/docker-compose.yml down -v
docker compose -f deploy/prod/docker-compose.yml --env-file deploy/prod/.env down -v- Passwords are hashed with BCrypt.
- JWT access tokens are signed with an HMAC secret configured by
APP_JWT_SECRET. - Role-based authorization is enforced in the Spring Security configuration.
- The React app stores the JWT in
localStoragefor portfolio/demo simplicity. This is acceptable for this project scope, but a high-security production application should consider stronger XSS controls, short token lifetimes, refresh-token rotation, or an HTTP-only cookie strategy. - Swagger and Actuator health are public in this demo so reviewers can inspect and verify the app quickly.
- Demo credentials in
.env.examplefiles are intentionally non-secret examples and should be changed in persistent environments.
This project is intentionally scoped for job applications, not real clinic operations.
- No real patient privacy, HIPAA, insurance, claims, or clinical compliance implementation.
- No refresh tokens, password reset, MFA, or account lockout.
- No frontend automated tests yet.
- No Testcontainers integration tests yet.
- No pagination/search on list endpoints yet.
- No user edit/disable UI yet.
- Screenshot files are placeholders until real screenshots are captured.
- Backend cannot connect locally: confirm PostgreSQL is running with
docker compose -f deploy/local/docker-compose.yml ps. - Password errors locally: use
DB_PASSWORD=clinic_passwordor setPOSTGRES_PASSWORDbefore creating the local database volume. - Port 5432 already in use: stop the other PostgreSQL service or change the local compose port mapping.
- Frontend API calls fail locally: confirm the backend is on port
8080andfrontend/.envhasVITE_API_BASE_URL=http://localhost:8080/api. - Login fails after changing
APP_ADMIN_PASSWORD: the bootstrap password is only used when the admin user is first created. Reset the database volume or change the password directly in the database. - JWT startup errors: make sure
APP_JWT_SECRETis at least 32 bytes. - Swagger UI is blank or unavailable: confirm the backend started successfully and Flyway migrations completed.
- Production frontend loads but API fails: confirm the
backendcontainer is healthy enough to respond and that Nginx is proxying/apithrough the frontend container. - Duplicate smoke-test data returns
409 Conflict: use a different patient email or service name, or reset volumes withdown -v.