Production-style backend for a ChatGPT-like chat system built with Java 21, Spring Boot, JPA, H2, and the OpenRouter LLM API.
Repository: https://github.com/imrajeevnayan/springboot-ai-chat-backend.git
This project demonstrates backend engineering skills that are directly useful in real-world AI products:
- Clean layered architecture with Controller, Service, Repository, DTO, and provider-client boundaries
- OpenRouter integration using a dedicated client with timeout, retry, and provider error handling
- Persistent conversation history with users, conversations, and messages
- Stateless REST APIs suitable for frontend, mobile app, or external client integration
- H2 local database setup for quick testing without external infrastructure
- Input validation, centralized error handling, and rate limiting
- Scalable design decisions documented for moving from local development to production
| Area | Technology |
|---|---|
| Language | Java 21 |
| Framework | Spring Boot 3.x |
| API | Spring Web REST |
| Persistence | Spring Data JPA, Hibernate |
| Local Database | H2 file-backed database |
| AI Provider | OpenRouter Chat Completions API |
| Build Tool | Maven |
| Testing | JUnit 5, Spring Boot Test, MockMvc |
| Monitoring | Spring Boot Actuator |
Client / Postman
|
v
ChatController
|
v
ChatService
|
+--> RateLimitService
|
+--> UserRepository / ConversationRepository / MessageRepository
|
+--> OpenRouterClient
|
v
OpenRouter API
controller: REST endpoints, request validation, HTTP status handlingservice: business flow, conversation context, rate limiting, persistence orchestrationrepository: database access using Spring Data JPAdomain: JPA entities for users, conversations, and messagesdto: API request and response modelsopenrouter: external AI provider integrationexception: centralized error mapping and clean JSON error responsesconfig: app configuration, WebClient setup, request path normalization
- Send prompts and receive AI responses
- Create new conversations automatically
- Continue existing conversations using
conversationId - Store full chat history per user
- List conversations with pagination
- List messages for a conversation
- Delete conversations safely by user ownership
- Use H2 database locally with persistent file storage
- Handle invalid input with validation errors
- Handle OpenRouter API failures using structured backend errors
- Normalize accidental trailing encoded whitespace in URLs from Postman
This backend is intentionally structured so it can grow beyond a local demo.
- Stateless API design: requests carry
userId, making the app easier to scale horizontally. - Provider isolation: OpenRouter logic is contained in
OpenRouterClient, so another LLM provider can be added without rewriting controllers. - Bounded context window: only recent messages are sent to the model to control latency, cost, and token usage.
- DTO pattern: API contracts are separated from database entities.
- Repository abstraction: persistence can move from H2 to PostgreSQL or MySQL with minimal business-layer changes.
- Central error handling: clients receive consistent JSON error responses.
- Config-driven behavior: model, rate limit, timeouts, and provider settings are environment configurable.
- Test profile isolation: tests use in-memory H2 and do not conflict with the running local file database.
- OpenRouter request timeout
- Retry support for transient provider/network failures
- Provider rate-limit handling
- Empty or malformed provider response handling
- Local per-user request rate limiting
- Central
RestExceptionHandler - Spring Boot Actuator health endpoint
- API key is read from environment variables, not stored in code or database.
- Request body validation protects prompt size and required fields.
- Conversation access checks combine
conversationIdanduserIdto avoid accidental cross-user access. - SQL injection risk is reduced by using Spring Data JPA parameter binding.
.gitignoreexcludes local database files and build output.
users
id
external_user_id
display_name
created_at
updated_at
conversations
id
user_id
title
created_at
updated_at
messages
id
conversation_id
role
content
model
created_at
Relationships:
- One user has many conversations.
- One conversation has many messages.
- Messages are ordered by
created_at.
Indexes:
users.external_user_idconversations(user_id, updated_at)messages(conversation_id, created_at)
Install:
- Java 21
- Maven 3.9+
- Postman
- OpenRouter API key
Verify Java:
java -versionVerify Maven:
mvn -versionFor the current PowerShell window:
$env:OPENROUTER_API_KEY="your_openrouter_api_key_here"Permanent Windows setup:
setx OPENROUTER_API_KEY "your_openrouter_api_key_here"After using setx, close PowerShell and open a new terminal.
The API key is used only by the backend to call OpenRouter. It is not stored in H2.
From the project root:
mvn spring-boot:runApplication URL:
http://localhost:8080
Health check:
GET http://localhost:8080/actuator/health
Expected response:
{
"status": "UP"
}Build the image:
docker build -t springboot-ai-chat-backend .Run the container:
docker run --rm -p 8080:8080 `
-e OPENROUTER_API_KEY="your_openrouter_api_key_here" `
-v ${PWD}/data:/app/data `
springboot-ai-chat-backendOpen:
http://localhost:8080
The default local profile uses file-backed H2:
jdbc:h2:file:./data/chat_backend
This keeps local chat history after app restarts.
H2 console:
http://localhost:8080/h2-console
Connection values:
JDBC URL: jdbc:h2:file:./data/chat_backend
User Name: sa
Password:
Leave password empty.
Useful SQL:
SELECT * FROM users;
SELECT * FROM conversations;
SELECT * FROM messages;| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/chat/messages |
Send prompt and receive AI response |
GET |
/api/v1/users/{userId}/conversations |
List conversations for a user |
GET |
/api/v1/conversations/{conversationId}/messages?userId={userId} |
List messages in a conversation |
DELETE |
/api/v1/conversations/{conversationId}?userId={userId} |
Delete a conversation |
GET |
/actuator/health |
Service health check |
Method: GET
URL: http://localhost:8080/actuator/health
Method: POST
URL: http://localhost:8080/api/v1/chat/messages
Headers:
Content-Type: application/json
Body:
{
"userId": "user-1",
"title": "First chat",
"prompt": "Hello, explain Spring Boot in one paragraph"
}Expected response:
{
"conversationId": "uuid",
"userMessageId": "uuid",
"assistantMessageId": "uuid",
"role": "assistant",
"content": "AI response text",
"model": "openai/gpt-4o-mini",
"createdAt": "timestamp"
}Copy conversationId from the first response.
{
"userId": "user-1",
"conversationId": "paste-conversation-id-here",
"prompt": "Now explain it with a small Java example"
}GET http://localhost:8080/api/v1/users/user-1/conversations?page=0&size=20
GET http://localhost:8080/api/v1/conversations/{conversationId}/messages?userId=user-1&page=0&size=50
DELETE http://localhost:8080/api/v1/conversations/{conversationId}?userId=user-1
Expected status:
204 No Content
Main config:
src/main/resources/application.yml
Local H2 config:
src/main/resources/application-local.yml
Optional PostgreSQL config:
src/main/resources/application-postgres.yml
Environment variables:
| Variable | Purpose | Default |
|---|---|---|
OPENROUTER_API_KEY |
OpenRouter API key | empty |
OPENROUTER_MODEL |
Default model | openai/gpt-4o-mini |
SERVER_PORT |
App port | 8080 |
CHAT_REQUESTS_PER_MINUTE |
Per-user local rate limit | 30 |
CHAT_PROMPT_MAX_LENGTH |
Maximum prompt length | 12000 |
mvn clean testExpected:
BUILD SUCCESS
Tests run against in-memory H2 using the test profile.
src/main/java/com/example/chatbackend
config Configuration, WebClient setup, URL normalization
controller REST controllers
domain JPA entities and enums
dto Request and response DTOs
exception Custom exceptions and global error handler
openrouter OpenRouter client and provider DTOs
repository Spring Data JPA repositories
service Business logic, chat flow, rate limiting
Set the key and restart the app:
$env:OPENROUTER_API_KEY="your_openrouter_api_key_here"
mvn spring-boot:runUse the exact URL:
http://localhost:8080/api/v1/chat/messages
Avoid trailing spaces or newlines in Postman.
Stop any existing Spring Boot process using the database, then restart:
mvn spring-boot:runRun on another port:
$env:SERVER_PORT="8081"
mvn spring-boot:runUse:
http://localhost:8081
This project is local-development ready with H2. For production, upgrade these areas:
- Replace H2 with PostgreSQL or MySQL.
- Add Flyway or Liquibase database migrations.
- Add Spring Security with JWT or OAuth2.
- Replace in-memory rate limiting with Redis or API gateway throttling.
- Add structured logging and distributed tracing.
- Add Docker and CI/CD pipeline.
- Add streaming responses for long AI completions.
- Add token counting and conversation summarization.
- Java backend development with Spring Boot
- REST API design and DTO modeling
- JPA entity design and relationship mapping
- External API integration with WebClient
- AI provider integration patterns
- Error handling and resilience
- Local database setup with H2
- Testable layered architecture
- Scalability-aware backend design

