A Spring Boot backend for a social platform — users post, comment and like, behind JWT authentication, email verification, password reset, and soft account deletion with a 30-day grace period. Fully documented, fully tested, and it starts with no configuration at all.
| Where | What |
|---|---|
| Documentation Central | The full docs site — setup, architecture, endpoint reference, data model, testing, known issues |
Swagger UI — /swagger-ui.html |
Interactive console. Click Authorize, paste a token from POST /auth/login, call anything |
OpenAPI spec — /v3/api-docs |
Machine-readable (.yaml for YAML). Checked in at doc/api-documentation.yml |
http-template/ |
Runnable .http files for the VS Code REST Client |
| Postman setup | Import the spec by URL — the collection stays in sync with the API |
cd documentation-central && npm install && npm start # docs at localhost:3000| Layer | Technology |
|---|---|
| Language | Java 17 |
| Framework | Spring Boot 3.4.4 — Web, Data JPA, Security, Validation, Mail, Cache, HATEOAS |
| Database | PostgreSQL 15+ (JSONB for post metadata) |
| Migrations | Flyway — schema is versioned, ddl-auto=validate |
| Auth | JWT (jjwt 0.11), BCrypt hashing, stateless sessions |
| Mapping | MapStruct 1.6.3 |
| API Docs | springdoc-openapi 2.8.6 → Swagger UI + OpenAPI 3 |
| Scheduling | Spring @Scheduled + ShedLock for multi-instance safety |
| Testing | JUnit 5 + Mockito 5 + MockMvc + AssertJ, JaCoCo coverage |
| Docs site | Docusaurus 3.10 (TypeScript, Mermaid), deployed on Vercel |
| Build | Gradle 8.13 |
| Mail (dev) | Mailpit |
- Authentication
- Registration with emailed verification token (24 h)
- Login by email → JWT (1 h, configurable)
- Password reset by link or one-time code, rate limited per address and per flow
- Password strength enforced identically at registration and reset
- Posts — full CRUD, owner-scoped reads, pagination and filtering
- Comments — nested under their post, author-only editing, pagination and filtering
- Likes — idempotent toggle endpoint, like counts, per-user like listing
- Users
- Profile, paginated and filterable listings
- Soft account deletion with a 30-day grace period and reactivation token
- Admin endpoints: force-delete, extend grace period, list pending deletions
- Role-based access —
USERgranted at registration,ADMINfor privileged endpoints - Background jobs — daily permanent-deletion sweep, hourly expired-token cleanup, both lock-protected
- Zero-config startup — every setting has a working default;
.envonly when you need to change one - 143 unit and slice tests that need no database (see Testing)
There is no /api prefix. Send Authorization: Bearer <token> on everything outside /auth/**.
A missing or invalid token gets 401; a valid token without the right ownership or role gets 403.
| Method | Path | Purpose |
|---|---|---|
POST |
/auth/registration |
Register (password: 8+ chars, uppercase, digit, special) |
POST |
/auth/login |
Log in, returns a JWT |
GET |
/auth/verify?token= |
Verify an email address |
GET |
/auth/registrationConfirm?token= |
Confirm from the emailed link |
POST |
/auth/forgot-password/email |
Request a reset link |
POST |
/auth/forgot-password/otp |
Request a reset one-time code |
POST |
/auth/reset-password |
Complete a reset with a token or OTP |
| Method | Path | Purpose |
|---|---|---|
GET |
/users/me |
Current user's profile |
GET |
/users/ |
All users (note the trailing slash) |
GET |
/users/paginated |
Users, paginated and filterable |
DELETE |
/users/me |
Soft-delete → 202, 30-day grace period |
POST |
/users/reactivate |
Restore an account inside the grace period |
| Method | Path | Purpose |
|---|---|---|
DELETE |
/users/admin/users/{id} |
Delete an account ({"forceDelete": true} skips the grace period) |
PUT |
/users/admin/users/{id}/extend-deletion |
Extend a grace period |
GET |
/users/admin/users/scheduled-deletion |
List accounts pending permanent deletion |
| Method | Path | Purpose |
|---|---|---|
POST |
/posts |
Create → 201 |
GET |
/posts |
All posts, paginated and filterable |
GET |
/posts/{id} |
Single post (403 if missing or not yours) |
PUT |
/posts/{id} |
Update |
DELETE |
/posts/{id} |
Delete → 204 |
GET |
/posts/my-posts |
Caller's posts |
GET |
/posts/my-posts/paginated |
Caller's posts, paginated |
GET |
/posts/user/{userId}/paginated |
One user's posts, paginated |
| Method | Path | Purpose |
|---|---|---|
POST |
/posts/{postId}/comments |
Add a comment → 201 |
GET |
/posts/{postId}/comments |
A post's comments, paginated and filterable |
GET |
/posts/{postId}/comments/{commentId} |
Single comment |
PUT |
/posts/{postId}/comments/{commentId} |
Update |
DELETE |
/posts/{postId}/comments/{commentId} |
Delete → 204 |
| Method | Path | Purpose |
|---|---|---|
POST |
/{postId}/likes/toggle |
Like or unlike; data holds the resulting state |
GET |
/{postId}/likes/count |
Like count (public posts only) |
GET |
/{postId}/likes/my-likes |
Caller's likes across all posts |
connectly-api/
├── build.gradle
├── README.md
├── LICENSE
├── .env.example every configurable setting
├── doc/
│ └── api-documentation.yml exported OpenAPI 3 spec
├── documentation-central/ Docusaurus docs site (Vercel)
│ ├── docs/
│ │ ├── getting-started/ installation, configuration
│ │ ├── api/ endpoint reference, errors, pagination
│ │ ├── architecture/ overview, security, scheduled tasks
│ │ ├── data-model/ schema + ERD
│ │ ├── testing/ the Mockito suite
│ │ └── reference/ known issues, further reading
│ ├── docusaurus.config.ts
│ └── vercel.json
├── src/
│ ├── main/
│ │ ├── java/com/jomariabejo/connectly_api/
│ │ │ ├── config/ security, JWT filter, OpenAPI, ShedLock
│ │ │ ├── controller/ REST endpoints
│ │ │ ├── dto/ request & response shapes
│ │ │ ├── exception/ domain exceptions + GlobalExceptionHandler
│ │ │ ├── mapper/ MapStruct entity ⇄ DTO
│ │ │ ├── model/ JPA entities
│ │ │ ├── repository/ Spring Data JPA
│ │ │ ├── scheduled/ background cleanup jobs
│ │ │ └── service/ business logic
│ │ └── resources/
│ │ ├── application.properties
│ │ ├── db/migration/ Flyway migrations
│ │ └── docs/http-template/ runnable .http request samples
│ └── test/java/com/jomariabejo/connectly_api/
│ ├── service/ 7 Mockito unit test classes
│ └── controller/ 6 @WebMvcTest slice classes
└── gradlew
- JDK 17+ (
java --version) - PostgreSQL 15+ running locally (
pg_isready) - Docker (optional — for the local mail catcher)
- Node 18+ (optional — only to run the docs site)
Gradle is not needed; the wrapper downloads it.
git clone https://github.com/jomariabejo/connectly-api.git
cd connectly-apicp .env.example .envEvery setting has a working local default, so you can skip this entirely. Copy the file only when something differs — a database password, an SMTP port, your own JWT secret. .env.example documents every variable; .env is gitignored.
Real environment variables take precedence over .env, so CI and production set the same names without shipping a file.
createdb connectly_dbThat is all — Flyway creates every table and seeds the roles on first start. Defaults to postgres / admin on localhost:5432; override with DB_URL, DB_USERNAME, DB_PASSWORD.
Registration and password reset both send email. Mailpit accepts the messages and shows them in a browser.
docker run -d --name mailpit -p 1025:1025 -p 8025:8025 axllent/mailpit./gradlew bootRun| API | http://localhost:8080 |
| Swagger UI | http://localhost:8080/swagger-ui.html |
| OpenAPI spec | http://localhost:8080/v3/api-docs |
| Mail inbox | http://localhost:8025 |
# 1. Register
curl -X POST http://localhost:8080/auth/registration \
-H 'Content-Type: application/json' \
-d '{"username":"someone","email":"[email protected]","password":"StrongPass1!"}'
# 2. Read the verification mail at http://localhost:8025, then:
curl "http://localhost:8080/auth/verify?token=THE_TOKEN_FROM_THE_EMAIL"
# 3. Log in and keep the token
JWT=$(curl -s -X POST http://localhost:8080/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"StrongPass1!"}' | jq -r .token)
# 4. Call something protected
curl http://localhost:8080/users/me -H "Authorization: Bearer $JWT"A 200 with your profile means everything is wired up.
143 tests across 13 classes. None of them need a database, a mail server, or a running application.
./gradlew unitTest # 143 tests — no database required
./gradlew test # adds ConnectlyApiApplicationTests, which needs PostgreSQL| Task | Runs | Needs PostgreSQL? |
|---|---|---|
unitTest |
Every Mockito unit test and controller slice | ❌ No |
test |
The above plus ConnectlyApiApplicationTests |
✅ Yes |
ConnectlyApiApplicationTests.contextLoads() is a @SpringBootTest, so it starts the whole application. unitTest excludes it by name — use it in CI.
Pure Mockito: @ExtendWith(MockitoExtension.class), @Mock collaborators, @InjectMocks subject. No Spring context.
| Class | What it tests |
|---|---|
PostServiceTest |
Author assignment, ownership on read/update/delete, pagination envelope, filter forwarding |
CommentServiceTest |
Post/author linking, author-only edits, getComment scoping to its {postId} |
UserServiceTest |
Soft-delete lifecycle — 30-day scheduling, reactivation, grace-period boundaries, dependant-ordered purge |
AuthenticationServiceTest |
Signup hashing, role grant, password strength, login, verification, both reset flows, rate limiting |
PostLikeServiceTest |
Toggle on/off, idempotent create, private posts reporting not-found |
JwtServiceTest |
Token round-trip, extra claims, expiry, wrong user, foreign signature |
RateLimitingServiceTest |
Threshold behaviour, per-address and per-action isolation, window expiry |
@WebMvcTest with MockMvc: real routing, real JSON serialization, real validation — mocked services.
| Class | What it tests |
|---|---|
AuthenticationControllerTest |
Registration, @Valid rejection, login payload, verification, both reset flows, the 429 path |
PostControllerTest |
CRUD status codes, @PageableDefault binding, filter switching, the deliberate 403s |
CommentControllerTest |
Nested routes, 201/204, author-only edits, pagination defaults |
UserControllerTest |
Profile, the 202 on DELETE /users/me, reactivation, admin endpoints |
PostLikeControllerTest |
Toggle semantics, like counting, regression guard on the route shape |
Coverage: ./gradlew test jacocoTestReport → build/reports/jacoco/test/html/.
Full conventions — @MockitoBean vs @MockBean, ReflectionTestUtils for @Value fields, the shared @ControllerSliceTest annotation — are in the testing guide.
Register → verify → login
POST /auth/registration
{"username":"someone","email":"[email protected]","password":"StrongPass1!"}
200 OK
{
"id": 1,
"username": "someone",
"email": "[email protected]",
"enabled": false,
"roles": ["USER"],
"autoReactivationEnabled": true
}
GET /auth/verify?token=a8561612-d690-4a47-8e5e-f9e5fb886a81
200 OK
Email verified successfully. You can now login.
POST /auth/login
{"email":"[email protected]","password":"StrongPass1!"}
200 OK
{"token":"eyJhbGciOiJIUzI1NiJ9…","expiresIn":3600000}
No password hash and no verification token in any response.
Paginated listing
GET /posts?page=1&size=5&sort=createdAt,desc
Authorization: Bearer <token>
200 OK
{
"content": [ … ],
"pageNumber": 1,
"pageSize": 5,
"totalElements": 12,
"totalPages": 3,
"hasNext": true,
"hasPrevious": true
}
Supplying any filter (title, content, postType, privacy, createdById) switches to the filtered query.
Like toggle
POST /1/likes/toggle
200 OK {"message":"Post liked successfully.","data":true}
POST /1/likes/toggle
200 OK {"message":"Post unliked successfully.","data":false}
GET /1/likes/count
200 OK {"message":"Total likes retrieved.","data":3}
One endpoint for both directions — data is the resulting state, not the action.
Account deletion (202, not 204)
DELETE /users/me
{"autoReactivationEnabled": true}
202 Accepted
{
"message": "Account has been marked for deletion",
"deletedAt": "2026-08-04T07:54:32",
"scheduledDeletionAt": "2026-09-03T07:54:32",
"gracePeriodDays": 30,
"autoReactivationEnabled": true
}
Deletion is scheduled, not done. The account survives for 30 more days and stays invisible to every query meanwhile.
Errors
# No token
401 {"status":401,"error":"Unauthorized",
"message":"Authentication required. Send a bearer token from POST /auth/login."}
# Valid token, someone else's post
403 {"status":403,"error":"Forbidden","message":"…is not authorized…"}
# Validation failure
400 {"status":400,"error":"Validation failed",
"message":"title: Title must be between 5 and 100 characters"}
# Weak password
400 {"status":400,"error":"Password too weak",
"message":"Password must be at least 8 characters long and contain
uppercase, number, and special character"}
# Unknown URL (authenticated)
404 {"status":404,"error":"Not Found","message":"No static resource no/such/route."}
Nothing is hardcoded. Every property is a placeholder with a fallback:
spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/connectly_db}Resolved in order — first one that exists wins:
- A real environment variable
- A
.envfile in the project root - The default in
application.properties
.env is read by Spring Boot itself via spring.config.import=optional:file:.env[.properties] — no dotenv library involved. Full variable reference in the configuration guide.
Important
The default JWT_SECRET is committed so a fresh clone runs with no setup — which means it is public. Generate your own with openssl rand -hex 32 for anything that is not your laptop.
Tip
Before deploying publicly: set SWAGGER_ENABLED=false and drop LOG_LEVEL_SECURITY to WARN.
- Schema is owned by Flyway. Migrations live in
src/main/resources/db/migration; Hibernate runs withddl-auto=validateand refuses to start if the entities and tables disagree. AddV2__your_change.sqlrather than editingV1. - Soft delete runs through every query. Repositories filter
deletedAt IS NULLexplicitly — a derived query added without that clause will leak deleted users' data. - Ownership is enforced in the service layer, not by path rules.
SecurityConfigurationonly distinguishes public /USER/ADMIN. GET /posts/{id}returns 403 for a missing post by design, so the endpoint cannot be used to discover which IDs exist.- Rate-limit counters are in memory — cleared on restart, not shared between instances.
ADMINhas no grant endpoint. Registration assignsUSER; promote via SQL (see Security).- Remaining rough edges are tracked honestly in Known Issues, each with the evidence behind it.
This project was developed with the assistance of AI tools, including Claude (Anthropic). AI was used for code suggestions, debugging, testing, and documentation.
All AI-generated outputs were reviewed, modified, and validated by the author.
MIT — see LICENSE.