Skip to content

Repository files navigation

Java 17 Spring Boot 3.4.4 PostgreSQL 16 Gradle 8.13 JWT Auth Flyway Swagger / OpenAPI 3 Docusaurus 3.10 143 tests passing 24 endpoints MIT License

🌐 Connectly API

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.


Documentation

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

Tech Stack

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

Features

  • 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 accessUSER granted at registration, ADMIN for privileged endpoints
  • Background jobs — daily permanent-deletion sweep, hourly expired-token cleanup, both lock-protected
  • Zero-config startup — every setting has a working default; .env only when you need to change one
  • 143 unit and slice tests that need no database (see Testing)

API Endpoints

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.

Authentication — public

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

Users

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

Users — admin only (ROLE_ADMIN)

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

Posts

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

Comments

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

Likes

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

Project Structure

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

Getting Started

Prerequisites

  • 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.

1. Clone

git clone https://github.com/jomariabejo/connectly-api.git
cd connectly-api

2. Configure (optional)

cp .env.example .env

Every 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.

3. Create the database

createdb connectly_db

That 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.

4. Run a mail catcher

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

5. Run the application

./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

Verify it works

# 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.


Testing

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

Two Gradle tasks

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.

Service unit tests — 84 tests

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

Controller slice tests — 59 tests

@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 jacocoTestReportbuild/reports/jacoco/test/html/.

Full conventions — @MockitoBean vs @MockBean, ReflectionTestUtils for @Value fields, the shared @ControllerSliceTest annotation — are in the testing guide.


Sample Requests & Responses

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."}

Configuration

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:

  1. A real environment variable
  2. A .env file in the project root
  3. 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.


Constraints and Notes

  • Schema is owned by Flyway. Migrations live in src/main/resources/db/migration; Hibernate runs with ddl-auto=validate and refuses to start if the entities and tables disagree. Add V2__your_change.sql rather than editing V1.
  • Soft delete runs through every query. Repositories filter deletedAt IS NULL explicitly — a derived query added without that clause will leak deleted users' data.
  • Ownership is enforced in the service layer, not by path rules. SecurityConfiguration only 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.
  • ADMIN has no grant endpoint. Registration assigns USER; promote via SQL (see Security).
  • Remaining rough edges are tracked honestly in Known Issues, each with the evidence behind it.

Acknowledgments

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.


License

MIT — see LICENSE.

About

Scalable backend for a social platform with user posts, comments, secure authentication, privacy controls, and role-based access.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages