π Live Demo: thinkingoutloud.xyz
Thinking Out Loud is a modern, high-performance, and feature-rich full-stack blogging platform. Designed for writers who value thoughtful, long-form content, the platform pairs a robust Spring Boot REST backend with a responsive, modern React frontend. It offers rich-text writing tools, nested commenting threads, secure role-based navigation, and paginated reading experiences.
- Secure Authentication & Identity: Custom user registration and login backed by stateless JWT (JSON Web Tokens) and password hashing using BCrypt.
- Rich Text Editor (TipTap): Admin users can compose articles using a fully customized TipTap editor integration supporting alignments, text highlights, underlines, embedded links, custom layout, and image attachments.
- Nested Commenting System: Support for interactive comment sections under each post. Features include single-level nested replies (preventing deeply nested spaghetti threads), edit/delete permissions, and "Blog Author" badges to easily identify the post creator in the comments.
- Persistent Dark Mode: A premium, eyes-friendly, low-contrast Material Dark theme (
#121212base) with a persistent theme toggle. - Responsive Ellipsis Navbar: Nav elements collapse into a vertical 3-dot ellipsis menu on mobile screens, expanding into a floating modal for actions.
- Custom Confirmation Dialogs: A reusable, animated
ConfirmationModalcomponent rendered via React Portals to bypass layout conflicts, replacing raw browserwindow.confirmpopups. - Paginated Feeds: Seamless page-by-page rendering of latest posts, reducing bandwidth and improving load times.
- Role-Based Access Control: Standard users (
ROLE_USER) can read blogs and comment, while only authorized creators (ROLE_ADMIN) can access administrative editors to write, update, or delete blog posts. - Docker Containerization: Production-ready, multi-stage Docker build separating the compilation phase from the execution environment to produce clean, compact runner images.
Writers compose articles on the frontend using a customized TipTap editor. Instead of using markdown or plain text, TipTap compiles the formatting blocks into standard, structured HTML.
- Database Representation: The generated HTML is sent via the API and stored in a PostgreSQL
TEXTcolumn (contentin theblogtable), preserving paragraph layouts, text alignments, hyperlinks, and underlines directly. - XSS Sanitization: To protect readers from malicious injections, the frontend routes the retrieved HTML through DOMPurify before inserting it into the DOM. This ensures all scripting elements, inline styles, or unsafe handlers are scrubbed clean while retaining the rich styling.
- Developer Code-Block Keymaps: Includes custom key mappings in the code-block extension to capture typing keydowns:
- Tab Key: Intercepts standard focus-shifts and inserts two indentation spaces (
) at the cursor. - Enter Key: Reads the leading spacing/tabs of the current line and automatically duplicates them on the newline, enabling automatic developer indents.
- Tab Key: Intercepts standard focus-shifts and inserts two indentation spaces (
Every blog post features a dedicated imageUrl string attribute.
- Writers specify a cover image url in the AdminEditor.jsx.
- The feed card renders a cropped card layout preview, and the BlogDetails.jsx view renders the cover image as a full-bleed banner highlighting the article context.
To foster discussion, each post includes a responsive comment thread structure with the following properties:
- Self-Referential Mapping: The database
commentstable uses a self-referential parent column (parent_id) allowing comment records to link directly to another comment as a reply. - Single-Level Constraint: To prevent nested UI clutter and endless horizontal margins, the system enforces a strict 1-level reply constraint. The backend business logic in CommentService.java checks if the target comment is already a child reply; if
parent.getParent() != null, it rejects the request with anIllegalArgumentException. - Author Identity Badge: If a user commenting on a blog post is the creator of the post itself, the system marks the comment response object with
isAuthor = trueand flags their username with a highlighted badge. - Cascading Delete Propagation: Engineered clean Hibernate bidirectional entity cascade mappings (
cascade = CascadeType.ALL, orphanRemoval = true). Deleting an article automatically wipes out its comments, and deleting a comment automatically deletes all of its nested replies.
- Language & Engine: Java 21 (Eclipse Temurin)
- Framework: Spring Boot 4.0.2 (Web, Security, JPA, Validation)
- Security: Spring Security + JWT
- Database: PostgreSQL
- ORM: Spring Data JPA & Hibernate
- Utilities: Lombok (boilerplate reduction)
- Bundler & Core: Vite 7.3.1 + React 18/19
- Data Fetching:
@tanstack/react-query(mutations, query caching, and status tracking) - State Management: Zustand
- Rich Editor:
@tiptap/react&@tiptap/starter-kit - HTTP Client: Axios (configured with request and response interceptors to handle automatic JWT headers and token expiration redirects)
- Sanitization: DOMPurify (preventing XSS attacks from parsed editor content)
- Routing: React Router DOM (v7)
The diagram below outlines the step-by-step request and data flow of the application:
flowchart LR
%% Style Definitions
classDef client fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px;
classDef interceptor fill:#e8f5e9,stroke:#4caf50,stroke-width:2px;
classDef security fill:#fff9c4,stroke:#fbc02d,stroke-width:2px;
classDef app fill:#ffe0b2,stroke:#ff9800,stroke-width:2px;
classDef db fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px;
subgraph ClientLayer ["1. Frontend Client (React)"]
UI["React UI Views<br>(BlogList, BlogDetails,<br>AdminEditor, Login/Register)"]:::client
Zustand["Zustand Auth Store<br>(Token, Role, User State)"]:::client
ReactQuery["React Query Cache<br>(Handles pagination & caching)"]:::client
UI <--> Zustand
UI <--> ReactQuery
end
subgraph TransportLayer ["2. HTTP Interceptor Layer"]
Axios["Axios HTTP Client"]:::interceptor
Interceptor["Request Interceptor<br>(JWT Expiration Check &<br>Bearer Token Attachment)"]:::interceptor
UI --> Axios
Axios --> Interceptor
end
subgraph SecurityLayer ["3. Backend Security (Spring Security)"]
Cors["CORS Filter<br>(Checks origins & headers)"]:::security
JwtFilter["JwtAuthenticationFilter<br>(Extracts & validates JWT,<br>sets SecurityContext)"]:::security
Interceptor -->|HTTP Request| Cors
Cors --> JwtFilter
end
subgraph ServiceLayer ["4. Application Controllers & Services"]
Controllers["REST Controllers<br>(AuthController, BlogController,<br>CommentController)"]:::app
Services["Services (Business Logic)<br>(AuthService, BlogService,<br>CommentService)"]:::app
Repos["JPA Repositories<br>(UserRepository, BlogRepository,<br>CommentRepository)"]:::app
JwtFilter --> Controllers
Controllers --> Services
Services --> Repos
end
subgraph DatabaseLayer ["5. Database Storage"]
DB[("PostgreSQL Database<br>(users, blog, comments tables)")]:::db
Repos <-->|Spring Data JPA / SQL| DB
end
The project is structured into two main independent directories:
ThinkingOutLoud-blog/
βββ ThinkingOutLoud/ # Backend Spring Boot Application
β βββ .mvn/ # Maven wrapper configuration
β βββ src/
β β βββ main/
β β β βββ java/com/blog/ThinkingOutLoud/
β β β β βββ config/ # CORS, Security filters, and JWT configuration
β β β β βββ controller/ # Auth, Blog, and Comment API Controllers
β β β β βββ dto/ # Request / Response DTOs
β β β β βββ entity/ # JPA Entities (User, Blog, Comment)
β β β β βββ exception/ # Custom Exceptions & Global REST Handler
β β β β βββ repository/ # Spring Data JPA Repository interfaces
β β β β βββ service/ # Core business logic (Auth, Blog, Comment, CustomUserDetail)
β β β βββ resources/
β β β βββ application.properties # Shared configurations & default port
β β β βββ application-dev.properties # Local development (localhost DB defaults)
β β β βββ application-prod.properties # Cloud / Render environment variables
β β βββ test/ # Backend Integration & Unit Tests
β βββ Dockerfile # Production Multi-stage Docker config
β βββ pom.xml # Maven dependencies & plugins
β βββ mvnw / mvnw.cmd # Maven wrapper scripts
β
βββ ThinkingOutLoud-frontend/ # Frontend Vite React App
βββ thinking-out-loud/
βββ public/ # Public static assets
βββ src/
β βββ api/ # Axios instances & client services (auth, blog, comments)
β βββ app/ # Routing definition (router.jsx)
β βββ components/ # Reusable UI widgets (NavBar, TextEditor components)
β βββ features/ # Domain-driven features (Blogs, admin, auth, comments)
β β βββ admin/ # Blog Creation / Edit forms & TipTab components
β β βββ auth/ # Login & Register views, auth state
β β βββ Blogs/ # Blog Lists, Single Blog Detail View
β β βββ comments/ # Comment Threads and Reply handlers
β βββ store/ # Zustand state storage
β βββ utils/ # Auth checks & navigation helpers
β βββ index.css # Core styling variables & typographic properties
β βββ main.jsx # Application initialization (React Query + DOM Entry)
βββ .env # Local environment credentials & API endpoints
βββ package.json # NPM packages & build scripts
βββ vite.config.js # Vite configuration
All endpoints are prefixed with /api. Here is a reference table showing routes, request permissions, and functions:
| Endpoint | Method | Description | Request Body | Access Level |
|---|---|---|---|---|
/api/auth/register |
POST |
Register a new account | { "username": "...", "password": "...", "role": "ROLE_USER" } |
Public |
/api/auth/login |
POST |
Authenticate and retrieve JWT token | { "username": "...", "password": "..." } |
Public |
| Endpoint | Method | Description | Request / Query Params | Access Level |
|---|---|---|---|---|
/api/blogs |
GET |
Retrieve a paginated list of blogs | Query: page, size, sort |
Public |
/api/blogs/{id} |
GET |
Retrieve details of a single post | Path variable {id} |
Public |
/api/blogs |
POST |
Create a new blog post | { "title": "...", "content": "...", "imageUrl": "..." } |
Admin Only |
/api/blogs/{id} |
PATCH |
Edit details of a blog post | { "title": "?", "content": "?", "imageUrl": "?" } |
Admin Only |
/api/blogs/{id} |
DELETE |
Delete a blog post | Path variable {id} |
Admin Only |
| Endpoint | Method | Description | Request Body | Access Level |
|---|---|---|---|---|
/api/blogs/{blogId}/comments |
GET |
Get nested comments for a blog post | Path variable {blogId} |
Public |
/api/blogs/{blogId}/comments |
POST |
Add a comment or write a reply | { "content": "...", "parentId": 123 } (parentId is optional) |
Authenticated |
/api/blogs/{blogId}/comments/{commentId} |
PATCH |
Edit comment content | { "content": "..." } |
Author Only |
/api/blogs/{blogId}/comments/{commentId} |
DELETE |
Delete a comment | Path variable {commentId} |
Author or Admin |
To run both the backend server and frontend client locally on your machine, follow these instructions.
- Java: JDK 21 installed.
- Node.js: Node.js 18+ and
npminstalled. - Database: PostgreSQL instance running locally.
-
Open the backend directory:
cd ThinkingOutLoud -
Create your local Database: Launch PostgreSQL and create a database named
blogdb:CREATE DATABASE blogdb;
-
Configure environment properties: By default, the backend runs in the
devprofile pointing to database details defined in application-dev.properties:- URL:
jdbc:postgresql://localhost:5432/blogdb - Username:
postgres - Password:
postgres
Set your custom JWT secret key and database credentials as environment variables or update the config:
# Set your JWT signing secret key export JWT_SECRET="yourSuperSecretSigningKeyThatIsAtLeast256BitsLong"
- URL:
-
Run the application: Run using the Maven wrapper:
# On Linux/macOS ./mvnw spring-boot:run # On Windows mvnw.cmd spring-boot:run
The backend server will start up on
http://localhost:8081(defined in application.properties).
- Open the frontend directory:
cd ../ThinkingOutLoud-frontend/thinking-out-loud - Install dependencies:
npm install
- Set environment variables:
Confirm the backend endpoint mapping inside the .env file:
VITE_API_BASE_URL=http://localhost:8081/api
- Run Vite Development server:
Open your browser and navigate to the printed address (usually
npm run dev
http://localhost:5173).
The application features a Dockerfile configured for multi-stage building. It ensures compilation is done on the fly, and the final image only contains the compiled JAR and runtime JDK, minimizing image footprint and security attack surface.
From the ThinkingOutLoud (backend) directory:
docker build -t thinking-out-loud-backend .You must supply the required configuration parameters via environmental injection:
docker run -d \
-p 8080:8080 \
-e PORT=8080 \
-e JWT_SECRET="yourSecretSignKey" \
-e SPRING_PROFILES_ACTIVE=prod \
-e SPRING_DATASOURCE_URL="jdbc:postgresql://your-cloud-database-host:5432/db_name" \
-e SPRING_DATASOURCE_USERNAME="your_db_username" \
-e SPRING_DATASOURCE_PASSWORD="your_db_password" \
--name blog-api-container \
thinking-out-loud-backendNote
The Docker container exposes port 8080. By passing -e PORT=8080 to the container, Spring Boot overrides its internal default port (8081) to match the container exposure limit seamlessly.
- Lombok Issues: If you see compilation or getter/setter missing errors in your IDE, ensure that Annotation Processing is enabled under compiler settings.
- Database Migrations: The project uses
spring.jpa.hibernate.ddl-auto=updatefor rapid development schemas. In a production scenario, it is highly recommended to transition to database migration frameworks like Flyway or Liquibase. - Token Expiry Routing: The Axios request interceptor checks if the token has expired before dispatching any request. If expired, it flushes the token and redirects the browser window to
/loginimmediately.