A RESTful blog application built with Spring Boot 3.3, Spring Security, and JWT authentication. It supports user registration, post management, and ownership-based access control.
- Tech Stack
- Project Structure
- Domain Model
- API Endpoints
- Security
- Configuration & Profiles
- Running the Application
- Running Tests
| Technology | Version | Purpose |
|---|---|---|
| Java | 21 | Programming language |
| Spring Boot | 3.3.9 | Application framework |
| Spring Security | (Boot) | Authentication & authorization |
| Spring Data JPA | (Boot) | Database access (ORM) |
| JJWT | 0.11.5 | JWT token generation & validation |
| MapStruct | 1.6.3 | DTO ↔ Entity mapping |
| Lombok | (Boot) | Boilerplate code reduction |
| H2 | (Boot) | In-memory database (dev/test) |
| PostgreSQL | 16 | Relational database (production) |
| SpringDoc OpenAPI (Swagger) | 2.3.0 | API documentation UI |
| Spring Boot Actuator | (Boot) | Health & monitoring endpoints |
| JaCoCo | 0.8.12 | Code coverage reporting |
| Docker Compose | — | PostgreSQL + Adminer local setup |
src/main/java/com/kte/blog_app/
├── BlogAppApplication.java # Application entry point
├── config/
│ ├── SecurityConfig.java # Spring Security configuration
│ ├── SwaggerConfig.java # OpenAPI/Swagger configuration
│ └── DataInitializer.java # Seed data on startup
├── controllers/
│ ├── AuthController.java # POST /api/v1/auth/register & /login
│ ├── PostController.java # CRUD for posts
│ ├── UserController.java # User read/update/delete
│ └── ui_controllers/ # Controller interfaces (Swagger annotations)
├── domain/
│ ├── entities/
│ │ ├── User.java # User JPA entity
│ │ ├── Post.java # Post JPA entity
│ │ └── PostStatus.java # Enum: DRAFT | PUBLISHED
│ └── dto/
│ ├── request/ # Incoming request DTOs
│ └── response/ # Outgoing response DTOs
├── exceptions/
│ ├── PostNotFoundException.java
│ ├── UserNotFoundException.java
│ ├── UserAlreadyExistsException.java
│ └── handler/ # Global exception handler (@RestControllerAdvice)
├── mappers/
│ ├��─ PostMapper.java # MapStruct Post ↔ DTO
│ └── UserMapper.java # MapStruct User ↔ DTO
├── repositories/
│ ├── PostRepository.java
│ └── UserRepository.java
├── security/
│ ├── JwtAuthenticationFilter.java # JWT filter (Bearer token extraction)
│ ├── BlogUserDetails.java # Custom UserDetails implementation
│ ├── AuthorizationService.java # Generic authorization checks
│ ├── PostSecurityService.java # Post-level access rules
│ └���─ UserSecurityService.java # User-level access rules
└── services/
├── AuthenticationService.java # Register, login, token generation
├── PostService.java # Post business logic interface
├── UserService.java # User business logic interface
└── impl/ # Service implementations
| Field | Type | Constraints |
|---|---|---|
| id | Long | Primary key, auto-gen |
| String | Unique, not null | |
| password | String | Not null (BCrypt encoded) |
| name | String | Not null |
| createDate | LocalDateTime | Set on creation |
| posts | List<Post> | One-to-many (cascade ALL) |
| Field | Type | Constraints |
|---|---|---|
| id | Long | Primary key, auto-gen |
| title | String | Not null |
| content | String (TEXT) | Not null |
| author | User | Many-to-one, not null |
| category | PostStatus | Enum: DRAFT / PUBLISHED |
| createDate | LocalDateTime | Set on persist |
| updateDate | LocalDateTime | Updated on each save |
| Method | Path | Auth Required | Description |
|---|---|---|---|
| POST | /register |
No | Register a new user |
| POST | /login |
No | Login and receive a JWT |
Register — request body:
{
"name": "John Doe",
"email": "john@example.com",
"password": "secret123"
}Login — request body:
{
"email": "john@example.com",
"password": "secret123"
}Auth response (both endpoints):
{
"token": "<JWT>",
"expiresIn": 86400
}| Method | Path | Auth Required | Description |
|---|---|---|---|
| POST | / |
✅ Yes | Create a post (authenticated user) |
| GET | /{id} |
No | Get a post by ID |
| GET | /category?category= |
No | Get posts filtered by category |
| GET | /search?authorId=&category= |
No | Get posts by author and category |
| PUT | /{id} |
✅ Owner only | Update a post |
| DELETE | /{id} |
✅ Owner only | Delete a post |
PostStatus values: DRAFT, PUBLISHED
| Method | Path | Auth Required | Description |
|---|---|---|---|
| GET | /{id} |
No | Get user by ID |
| GET | /email/{email} |
No | Get user by email |
| PUT | /{id} |
✅ Owner only | Update user profile |
| DELETE | /{id} |
✅ Owner only | Delete user account |
- Authentication: JWT Bearer tokens passed via the
Authorizationheader (Bearer <token>). - Token expiration: 24 hours (86 400 000 ms).
- JWT secret: Read from the
JWT_SECRETenvironment variable. - Filter:
JwtAuthenticationFilterruns on every request, validates the token, and populates the Spring Security context. - Method-level authorization:
PostSecurityService— only the post owner canPUTorDELETEa post (@PreAuthorize).UserSecurityService— only the account owner canPUTorDELETEa user.
- H2 in-memory database (
jdbc:h2:mem:blogforumdb) - JWT secret from
$JWT_SECRET - Actuator exposes
healthandinfo
- H2 in-memory, DDL strategy:
create-drop - H2 Console enabled at
/h2-console - Server port:
8888(override with$SERVER_PORT) - Verbose DEBUG/TRACE logging
- PostgreSQL via
$DB_URL/$DB_USERNAME/$DB_PASSWORD - DDL strategy:
validate(schema must already exist) - Server port:
8181(override with$SERVER_PORT) - Log file:
logs/blog-app.log(10 MB max, 30-day retention)
| Variable | Profile | Description |
|---|---|---|
JWT_SECRET |
All | JWT HMAC signing secret |
DB_URL |
prod | PostgreSQL JDBC URL |
DB_USERNAME |
prod / Docker | Database username |
DB_PASSWORD |
prod / Docker | Database password |
SERVER_PORT |
Optional | Overrides the default port |
$env:JWT_SECRET = "your-very-secret-key"
./mvnw spring-boot:run| URL | Description |
|---|---|
http://localhost:8888 |
REST API base URL |
http://localhost:8888/swagger-ui.html |
Swagger UI |
http://localhost:8888/h2-console |
H2 Database Console |
1. Create a .env file at the project root:
DB_USERNAME=postgres
DB_PASSWORD=yourpassword
JWT_SECRET=your-very-secret-key2. Start PostgreSQL and Adminer:
docker-compose up -d3. Run the application with the prod profile:
$env:SPRING_PROFILES_ACTIVE = "prod"
$env:DB_URL = "jdbc:postgresql://localhost:5432/blogappDB"
$env:DB_USERNAME = "postgres"
$env:DB_PASSWORD = "yourpassword"
$env:JWT_SECRET = "your-very-secret-key"
./mvnw spring-boot:run| URL | Description |
|---|---|
http://localhost:8181 |
REST API base URL |
http://localhost:8989 |
Adminer (DB administration UI) |
# Run all tests and generate JaCoCo coverage report
./mvnw testAfter the build, open the coverage report at:
target/site/jacoco/index.html
The test suite covers: controllers, services, mappers, repositories, security services, and the global exception handler — using JUnit 5, Mockito, and Spring Security Test.