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": "user@example.com",
"password": "secret"
}Response: 200 OK with cookies set.
Creates a new user account. Requires ADMIN role.
Request body:
{
"name": "John Doe",
"email": "john@example.com",
"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": "new@example.com"
}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.