A full-stack car rental application built with Spring Boot 4 (Java 25) + React 19, featuring JWT cookie-based authentication, role-based access control, soft-delete with Hibernate
@SQLRestriction, MapStruct DTO mapping, and Docker Compose deployment. Built as a learning project exploring enterprise Java patterns.
- 🔐 JWT HttpOnly cookie authentication — XSS-safe token storage
- 👥 Role-based access control (
USER,ADMIN) via Spring Security@PreAuthorize - 🚗 Car & brand catalog with admin CRUD
- 📅 Reservation system with date-range conflict detection and pessimistic-style overlap check
- 🗑 Soft delete on all entities via
@SQLRestriction("is_deleted = false") - 🔄 MapStruct for entity ↔ DTO mapping (zero boilerplate)
- 📊 Transactional service layer (
@Transactional(readOnly = true)at class level, write overrides) - 🧱 Global exception handler with typed business exceptions
- 🐳 Docker Compose — MySQL + Spring Boot + Nginx React SPA, one command
- 🏠 Modern responsive UI — React 19 + Vite + Tailwind + shadcn-style components
- 🖼 Hero carousel + search bar on homepage; availability query for date-range filter
- 📱 RTK Query for data fetching with tag-based cache invalidation
| Layer | Technology |
|---|---|
| Language | Java 25 |
| Framework | Spring Boot 4.0.5 |
| Web | Spring Web MVC |
| Security | Spring Security 7 + JWT (jjwt 0.12.6) |
| ORM | Spring Data JPA + Hibernate 7 |
| Mapping | MapStruct 1.5.5 |
| Validation | Jakarta Bean Validation (Hibernate Validator) |
| Database | MySQL 8.0 |
| Boilerplate | Lombok |
| Layer | Technology |
|---|---|
| Framework | React 19 + Vite |
| Styling | Tailwind CSS v4 + shadcn-style primitives |
| State / data | Redux Toolkit + RTK Query |
| Routing | React Router v7 |
| Icons | lucide-react |
- Docker multi-stage builds (JDK 25 for build → JRE 25 for runtime)
- Docker Compose orchestration (MySQL + backend + Nginx frontend)
- Nginx serving built SPA + reverse-proxying
/api/*to backend
rentacarproject/
├── src/main/java/com/rentacar/rentacarproject/
│ ├── controller/ # REST controllers (Auth, Brand, Car, Reservation)
│ ├── service/ # Business logic + @Transactional
│ ├── repository/ # Spring Data JPA interfaces
│ ├── entity/ # JPA entities (User, Brand, Car, Reservation)
│ ├── dto/ # Request/response DTOs (auth, brand, car, reservation)
│ ├── mapper/ # MapStruct mapper interfaces
│ ├── security/ # JwtService, JwtAuthFilter, SecurityService
│ ├── exception/ # AppException + GlobalExceptionHandler
│ └── enums/ # Role, Status, Color, FuelType, Transmission, ReservationStatus
│
├── src/main/resources/
│ └── application.properties # Spring config (env-var driven)
│
├── frontend/
│ ├── src/
│ │ ├── app/ # RTK store + baseApi
│ │ ├── features/ # auth, brand, car, reservation (each feature owns its API)
│ │ ├── pages/ # HomePage, CarListPage, CarDetailPage, ...
│ │ ├── components/ # Layout, Header, HeroCarousel, SearchBar, ui/*
│ │ └── hooks/ # useAuth
│ ├── nginx.conf # SPA fallback + API proxy
│ └── Dockerfile
│
├── Dockerfile # Multi-stage Spring Boot image
├── docker-compose.yml # MySQL + backend + frontend
├── .env.example # Env var template
└── pom.xml # Maven config
- Docker + Docker Compose (easiest path)
- Or: Java 25 + Maven + Node 22 + MySQL 8 (local dev)
# 1. Clone
git clone https://github.com/RRimeKS/rentacarproject.git
cd rentacarproject
# 2. Configure env
cp .env.example .env
# Edit .env — set MYSQL_ROOT_PASSWORD, JWT_SECRET, etc.
# 3. Build & run
docker compose up --buildVisit:
- Frontend: http://localhost:5000
- Backend API: http://localhost:8080
- MySQL (for DB tools): localhost:3307
Backend:
./mvnw spring-boot:run # port 8080Frontend:
cd frontend
npm install
npm run dev # port 5173Frontend dev server proxies /api/* to http://localhost:8080 automatically (see vite.config.js).
Register a normal user via POST /auth/register, then elevate in MySQL:
UPDATE users SET role = 'ADMIN' WHERE email = 'you@example.com';POST /auth/register Public
POST /auth/login Public (sets HttpOnly JWT cookie)
GET /auth/me Authenticated (returns current user)
POST /auth/logout Public (clears cookie)
GET /brand/all Public
GET /brand/detail/{id} Public
POST /brand/create ADMIN
PUT /brand/update/{id} ADMIN
DELETE /brand/delete/{id} ADMIN (soft delete, blocks if cars attached)
GET /car/all Public
GET /car/detail/{id} Public
GET /car/available Public — query by pickupDate/returnDate
POST /car/create ADMIN
PUT /car/{id} ADMIN
DELETE /car/{id} ADMIN (soft delete, blocks RESERVED/RENTED)
POST /reservation/create Authenticated
GET /reservation/my Authenticated (own reservations)
GET /reservation/all ADMIN
GET /reservation/{id} Owner or ADMIN
POST /reservation/{id}/cancel Owner or ADMIN
- Class-level
@Transactional(readOnly = true)on every service; write methods override with@Transactional— enables Hibernate flush skipping on reads @EntityGraphon repositories to avoid N+1 on join-heavy queries (e.g., Car → Brand)- Hibernate
@SQLRestriction— all read queries auto-filter soft-deleted rows - JpaRepository method naming convention — no JPQL needed for simple filters (
findAllByIsDeletedFalse,existsByBrandIdAndIsDeletedFalse) - Custom
@Querywith native SQL — for availability search withNOT EXISTSsubquery - MapStruct with
nullValuePropertyMappingStrategy = IGNORE— null fields in update requests don't overwrite existing entity fields - Frontend feature-based structure — each feature owns its API slice, injected into a single
baseApi - RTK Query tag invalidation — cache stays coherent across related resources (e.g., creating a reservation invalidates
CarLIST)
- JWT in HttpOnly + Secure + SameSite=Lax cookies (XSS-safe)
- Cookie path
/so protected endpoints receive it (not just/auth/*) @PreAuthorizemethod-level guards (hasRole('ADMIN'),isAuthenticated())- Custom
AccessDeniedHandler+AuthenticationEntryPoint→ JSON error responses - Global
GlobalExceptionHandlerfor validation, auth, and business errors .envgit-ignored — no secrets committed- Env-var driven config (
${SPRING_DATASOURCE_URL:default})
- Multi-stage backend Dockerfile — JDK 25 builds
.jar, JRE 25 runs it (~250MB final image) - Frontend multi-stage — Node 22 Alpine builds, Nginx Alpine serves (~30MB final image)
- Backend connects to MySQL via service name
db:3306(Docker network) - Frontend Nginx proxies
/api/*→http://backend:8080 - Healthcheck on MySQL +
depends_on: service_healthyprevents boot-time race
This project was built as a learning exercise to explore enterprise Java patterns:
- Why
@Datais risky on JPA entities (→@ToString.Excludeon collections) @SQLRestrictionvs manual filter methods — trade-offs@SQLRestriction+ restore-on-create conflicts (solved with native query)- MapStruct annotation processor setup alongside Lombok
- Spring Security filter chain order +
/errorpath exception - JPA bidirectional relations and the
mappedByside - Docker Compose env var precedence and volume persistence
MIT — free to use, fork, learn from.