A production-ready Web API starter project built with Java and Spring Boot, designed to be the foundation for new backend applications. It comes pre-configured with JWT authentication via cookies, role-based access control, audit fields, and a clean, layered architecture — so you can skip the boilerplate and start building features right away.
- Overview
- Features
- Tech Stack
- Project Structure
- Authentication Flow
- API Endpoints
- Configuration
- Running Locally
- Docker
- License
Starter Java Spring provides a solid, opinionated starting point for building RESTful Web APIs. Rather than setting up security, authentication, and user management from scratch on every new project, this template gives you all of that out of the box, ready to extend.
The project is intentionally database-agnostic at its core - while it ships configured for MySQL, swapping to PostgreSQL, MariaDB, or any other JPA-compatible database requires nothing more than changing the connector dependency and the datasource URL.
- JWT Authentication via HttpOnly Cookies - stateless authentication using short-lived access tokens (15 minutes) and long-lived refresh tokens (7 days), both stored in HttpOnly cookies to mitigate XSS risks.
- Role-Based Access Control (RBAC) - two built-in roles (
ADMINandUSER) with granular method-level authorization using Spring Security's@PreAuthorize. - Refresh Token Strategy - persistent refresh tokens stored in the database, with the ability to invalidate sessions individually.
- Audit Fields - entities automatically track
createdAt,updatedAt,createdBy,updatedBy, and soft-delete viadeletedAt, powered by Spring Data JPA Auditing. - Soft Delete - deleting a user sets their
deletedAttimestamp rather than removing the record, and automatically disables their login. - SPA Support - the security configuration allows serving a Single Page Application (React, Vue, etc.) from the same server, with all non-API routes forwarded to
index.html. - Configurable CORS - allowed origins are externalized to environment variables, making multi-environment deployments straightforward.
- Global Exception Handling - consistent error responses across the API via
GlobalExceptionHandlerand custom exception types.
| Layer | Technology |
|---|---|
| Language | Java 21 |
| Framework | Spring Boot 4.0.6 |
| Security | Spring Security + JWT |
| Persistence | Spring Data JPA + Hibernate |
| Database | MySQL (swappable) |
| Build Tool | Maven |
| Utilities | Lombok |
src/main/java/com/victorseidel/starter_project/
│
├── audit/
│ ├── Auditing.java # Enables JPA auditing (@EnableJpaAuditing)
│ └── AuditorAwareImpl.java # Resolves the currently authenticated user for audit fields
│
├── configurations/
│ ├── SecurityConfiguration.java # Security filter chain, CORS, password encoder, session policy
│ └── SpaConfiguration.java # Forwards unknown routes to index.html for SPA support
│
├── controllers/
│ ├── AuthController.java # Login, logout, register, and token refresh endpoints
│ └── UserController.java # CRUD operations for users
│
├── dto/
│ └── user/
│ ├── LoginRequestDTO.java
│ ├── LoginResponseDTO.java
│ ├── RegisterRequestDTO.java
│ ├── UpdateUserRequestDTO.java
│ └── UserResponseDTO.java
│
├── exceptions/
│ ├── BadRequestException.java
│ ├── ForbiddenException.java
│ ├── NotFoundException.java
│ ├── ErrorResponse.java # Standardized error response body
│ └── GlobalExceptionHandler.java # Catches exceptions and maps them to HTTP responses
│
├── filters/
│ └── SecurityFilter.java # Reads JWT from cookies and authenticates the request
│
├── models/
│ ├── RefreshToken.java # Persistent refresh token entity
│ ├── User.java # User entity with audit fields and soft delete
│ └── UserPrincipal.java # UserDetails wrapper, exposes roles and account state
│
├── repositories/
│ ├── RefreshTokenRepository.java
│ └── UserRepository.java
│
├── services/
│ ├── AuthService.java # Login, logout, register, and token refresh logic
│ ├── CookieService.java # Builds, sets, and clears HttpOnly cookies
│ ├── RefreshTokenService.java # Creates and validates refresh tokens
│ ├── UserPrincipalService.java # Loads UserDetails by email (used by Spring Security)
│ └── UserService.java # User CRUD business logic
│
├── types/
│ └── UserRole.java # Enum: ADMIN, USER
│
└── utils/
└── AuthUtil.java # Helper to retrieve the authenticated user from the context
This project uses a dual-token, cookie-based authentication strategy:
┌─────────────────────────────────────────────────────────────────┐
│ Login Flow │
│ │
│ Client ──POST /api/auth/login──────────────────► AuthService │
│ │ │
│ Validate password │
│ │ │
│ Generate Access Token │
│ (JWT, expires 15min) │
│ │ │
│ Generate Refresh Token │
│ (stored in DB, 7 days) │
│ │ │
│ Client ◄── Set HttpOnly Cookies (access + refresh) ──┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Request Flow │
│ │
│ Client ──Any request (cookie sent automatically)──► Filter │
│ │ │
│ Extract access_token │
│ from cookie │
│ │ │
│ Validate JWT signature │
│ & expiration │
│ │ │
│ Set SecurityContext │
│ │ │
│ Client ◄── Response ─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Refresh Flow │
│ │
│ Client ──POST /api/auth/refresh────────────────► AuthService │
│ (refresh_token cookie sent automatically) │ │
│ │ │
│ Validate refresh token │
│ against DB record │
│ │ │
│ Issue new access token │
│ │ │
│ Client ◄── New access_token cookie ──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Cookie details:
| Cookie | Lifetime | Path | HttpOnly | Secure |
|---|---|---|---|---|
starter_project_access_token |
15 minutes | / |
✅ | configurable |
starter_project_refresh_token |
7 days | /api/auth/refresh |
✅ | configurable |
The refresh token cookie is scoped to /api/auth/refresh only, so it is never sent to other endpoints - reducing the attack surface.
The project ships with two roles:
| Role | Granted Authorities |
|---|---|
ADMIN |
ROLE_ADMIN, ROLE_USER |
USER |
ROLE_USER |
Default endpoint permissions:
| Method | Endpoint | Required Role |
|---|---|---|
POST |
/api/auth/login |
Public |
POST |
/api/auth/register |
ADMIN |
POST |
/api/auth/logout |
Authenticated |
POST |
/api/auth/refresh |
Authenticated (via cookie) |
GET |
/api/users |
ADMIN |
GET |
/api/users/{id} |
Owner or ADMIN |
PUT |
/api/users/{id} |
Owner or ADMIN |
DELETE |
/api/users/{id} |
Owner or ADMIN |
Users can only access or modify their own data. Admins can access any user.
Authenticates a user and sets access + refresh token cookies.
Request body:
{
"email": "[email protected]",
"password": "secret"
}Response: 200 OK with cookies set.
Creates a new user account. Requires ADMIN role.
Request body:
{
"name": "John Doe",
"email": "[email protected]",
"password": "secret",
"role": "USER"
}Response: 201 Created
Clears authentication cookies and invalidates the refresh token.
Response: 204 No Content
Issues a new access token using the refresh token cookie.
Response: 200 OK with a new access token cookie.
Returns a paginated list of users. Requires ADMIN role.
Returns a single user by ID. Accessible by the user themselves or an admin.
Updates a user's data. Accessible by the user themselves or an admin.
Request body:
{
"name": "New Name",
"email": "[email protected]"
}Soft-deletes a user (sets deletedAt). Accessible by the user themselves or an admin.
All sensitive and environment-specific values are externalized. Create a .env file or set environment variables before running:
| Variable | Description | Default |
|---|---|---|
PORT |
Server port | 3000 |
DB_HOST |
Database host | localhost |
DB_PORT |
Database port | 3306 |
DB_NAME |
Database name | (required) |
DB_USER |
Database username | root |
DB_PASS |
Database password | (required) |
JWT_SECRET |
Secret key for signing JWTs | my-secret-key |
⚠️ Always overrideJWT_SECRETin production with a long, random string. Never use the default.
Other configurable properties in application.properties:
# CORS — comma-separated list of allowed origins
app.cors.allowed-origins=http://localhost:3000
# Refresh token expiration
app.token.refresh.expiration-days=7
# Cookie settings
app.cookie.secure=true # Set to true in production (requires HTTPS)
app.cookie.same-site=Strict- Java 21+
- Maven 4.0+
- MySQL (or compatible database)
-
Clone the repository:
git clone https://github.com/victorSeidel/starter-java-spring.git cd starter-java-spring -
Create your database:
CREATE DATABASE starter_db;
-
Set environment variables:
DB_NAME=starter_db DB_PASS=yourpassword JWT_SECRET=a-very-long-and-random-secret-key
-
Run the application:
./mvnw spring-boot:run
The API will be available at
http://localhost:3000.
To use a different database (e.g., PostgreSQL):
-
Replace the MySQL connector in
pom.xml:<!-- Remove: --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Add: --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency>
-
Update
application.properties:spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME} spring.datasource.driver-class-name=org.postgresql.Driver
No other changes are required - JPA/Hibernate handles the rest.
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 3000
ENTRYPOINT ["java", "-jar", "app.jar"]services:
db:
image: mysql:8
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASS}
MYSQL_DATABASE: ${DB_NAME}
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
api:
build: .
restart: always
ports:
- "${PORT:-3000}:3000"
environment:
PORT: ${PORT:-3000}
DB_HOST: db
DB_PORT: 3306
DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER:-root}
DB_PASS: ${DB_PASS}
JWT_SECRET: ${JWT_SECRET}
depends_on:
- db
volumes:
mysql_data:PORT=3000
DB_HOST=localhost
DB_PORT=3306
DB_NAME=
DB_USER=root
DB_PASS=
JWT_SECRET=my-secret-keyRunning with Docker Compose:
cp .env.example .env # fill in your values
docker compose up --buildThis project is licensed under the MIT License.