Spring Boot 3 / Java 17 REST API for a blogging platform. Public read access, JWT-authenticated writes, and role-based admin operations.
Recruiters: start with Security model, then run ./mvnw test. That suite is the proof that unauthenticated writes are rejected and ROLE_USER cannot perform admin actions.
- Users register and log in (
/api/auth/register,/api/auth/login) - Authors create posts under a category, with pagination, search, comments, and image upload
- Admins manage categories and user accounts
- Schema is versioned with Flyway; the HTTP contract is documented in Swagger
| Layer | Choice |
|---|---|
| Runtime | Java 17, Spring Boot 3.0 |
| API | Spring Web, Bean Validation, springdoc OpenAPI |
| Security | Spring Security 6, JWT (JJWT), BCrypt, method security |
| Persistence | Spring Data JPA, Flyway, MySQL 8 (H2 in tests) |
| Ops | Actuator health/info, Docker Compose, GitHub Actions |
Client
-> AuthController / resource controllers
-> JwtAuthenticationFilter (Bearer token)
-> SecurityFilterChain + @PreAuthorize
-> Service interfaces (@Transactional)
-> Spring Data JPA repositories
-> MySQL (Flyway migrations)
DTOs (payloads) are mapped with ModelMapper so JPA entities are not returned as-is. Passwords are write-only in JSON and stored hashed.
| Path | Rule |
|---|---|
POST /api/auth/register, POST /api/auth/login |
Public |
GET /api/posts/**, GET /api/categories/**, GET /api/post/image/** |
Public |
POST /api/user/{id}/category/{id}/posts, comments, image upload |
Authenticated (ROLE_USER or ROLE_ADMIN) |
| Category mutations, list/delete users | ROLE_ADMIN |
DELETE /api/users/{id} |
ROLE_ADMIN and @PreAuthorize |
Tokens are stateless (SessionCreationPolicy.STATELESS). Invalid or missing Bearer tokens return 401; authenticated users without the required role return 403.
Default admin (local/demo only): admin@blog.local / Admin@12345. Override with ADMIN_EMAIL and ADMIN_PASSWORD.
./mvnw testcp .env.example .env
docker compose up --build- API: http://localhost:8010
- Swagger: http://localhost:8010/swagger-ui.html
- Health: http://localhost:8010/actuator/health
Create an empty blog_app database, then:
export MYSQL_PASSWORD=your-password
export JWT_SECRET=a-long-random-secret-of-at-least-32-chars
./mvnw spring-boot:runFlyway applies V1__init_schema.sql on startup (ddl-auto=validate). Use a fresh database; do not point this at a schema created by older ddl-auto=update runs.
# Register
curl -s -X POST http://localhost:8010/api/auth/register \
-H 'Content-Type: application/json' \
-d '{"name":"Ada","email":"ada@blog.local","password":"Pass1234","about":"author"}'
# Login
TOKEN=$(curl -s -X POST http://localhost:8010/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"ada@blog.local","password":"Pass1234"}' | jq -r .token)
# Create a post (needs an admin-created category first)
curl -s -X POST http://localhost:8010/api/user/2/category/1/posts \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"title":"Hello","content":"First post"}'In Swagger, use Authorize and paste Bearer <token>.
| Variable | Purpose |
|---|---|
MYSQL_PASSWORD |
Database password (not committed) |
JWT_SECRET |
HMAC key, at least 32 characters |
ADMIN_EMAIL / ADMIN_PASSWORD |
Bootstrap admin |
SPRING_DATASOURCE_URL |
JDBC URL (Compose sets this to the mysql service) |
Users, categories, posts (CRUD, filter by author/category, title search, pagination), comments, image upload/download. Pagination defaults: page 0, size 10, sort postId.