DEV Community

Cover image for Building AI Prompt Lab with Java 21, Spring Boot and React 19
CertosinoLab
CertosinoLab

Posted on • Originally published at certosinolab.blogspot.com

Building AI Prompt Lab with Java 21, Spring Boot and React 19

AI Prompt Lab is a full-stack application I built around a simple idea: managing reusable AI prompts should feel like working with any other structured application asset, rather than keeping them scattered across notes, text files or chat histories.

The project combines Java 21, Spring Boot, React 19, TypeScript, PostgreSQL and OpenRouter in a compact architecture that covers the main concerns of a modern web application: authentication, authorization, persistence, external API integration and management of sensitive configuration.

I deliberately kept the system relatively simple. The goal was not to introduce architectural patterns for their own sake, but to build a clean application where each technology has a clear responsibility.


The project

AI Prompt Lab provides an authenticated workspace where users can create, update, organize and reuse prompts for generative AI models.

Each user has an independent prompt collection and can configure an OpenRouter account to interact with different language models through the application.

At a high level, the application provides:

  • user authentication;
  • role-based authorization;
  • personal prompt management;
  • OpenRouter configuration;
  • AI chat functionality;
  • encrypted storage of external API credentials;
  • administrative user management.

From an architectural point of view, this makes the project more interesting than a conventional CRUD application while still remaining small enough to keep the overall design easy to reason about.


Technology stack

The repository is divided into two independent applications:

backend/
frontend/
Enter fullscreen mode Exit fullscreen mode

Backend

  • Java 21
  • Spring Boot
  • Spring Web
  • Spring Data JPA
  • Spring Security
  • PostgreSQL
  • Flyway
  • OpenRouter API

Frontend

  • React 19
  • TypeScript
  • Vite
  • React Router
  • React Context

The stack is intentionally conventional.

There is no microservice decomposition, no external state management library on the frontend and no additional infrastructure that the application does not currently require.

For this scope, keeping the system as a modular monolith provides a much better balance between maintainability, deployment complexity and development speed.


Backend architecture

The Spring Boot application follows a traditional layered structure:

controller
service
repository
model
config
Enter fullscreen mode Exit fullscreen mode

The separation is straightforward.

Controllers define the HTTP API, services contain application logic, repositories encapsulate persistence, and configuration components handle cross-cutting concerns such as security and encryption.

I prefer this approach for an application of this size because the control flow remains explicit. A request enters through a controller, moves through the service layer and reaches the persistence layer without introducing unnecessary indirection.

Prompt management

Prompt management is the central domain of the application.

Authenticated users can create, edit, delete and retrieve prompts associated with their account.

The backend also supports sorting directly at the repository level, keeping data-oriented operations close to the persistence layer instead of reimplementing them in the client.

The result is a small REST API with predictable resource-oriented operations and a clean separation between client-side presentation and server-side data access.


Authentication and authorization

Authentication is handled entirely by Spring Security.

Passwords are persisted using BCrypt hashing, while authenticated sessions are represented by a server-issued token stored in an HTTP-only cookie.

The browser sends the cookie automatically with subsequent requests, and a custom Spring Security filter restores the authenticated user before the request reaches the application layer.

The flow is essentially:

Login request
      |
      v
Credential verification
      |
      v
Session token creation
      |
      v
HTTP-only cookie
      |
      v
Spring Security filter
      |
      v
Authenticated request
Enter fullscreen mode Exit fullscreen mode

Authorization is enforced on the backend rather than being delegated to the user interface.

The application distinguishes between regular users and administrators, and administrative endpoints are protected through Spring Security.

The frontend can therefore use roles to adapt navigation and presentation, while the server remains the authoritative boundary for access control.


PostgreSQL and schema management

PostgreSQL provides the persistence layer, with Spring Data JPA handling repository access and entity mapping.

Schema evolution is managed through Flyway migrations.

I prefer keeping database changes explicit and versioned alongside the application rather than depending on automatic schema mutation at runtime.

This gives the database the same kind of traceability as the rest of the codebase:

V1__init_database.sql
V2__future_change.sql
V3__another_change.sql
Enter fullscreen mode Exit fullscreen mode

It also makes the application easier to move between environments because schema creation and evolution are part of a repeatable process rather than a manual deployment step.


OpenRouter integration

AI requests are handled by the backend rather than being sent directly from React to the external provider.

The frontend sends the conversation to the Spring Boot API, the backend loads the current user's AI configuration and then performs the request to OpenRouter.

React
  |
  v
Spring Boot API
  |
  v
User AI configuration
  |
  v
OpenRouter
  |
  v
Language model
  |
  v
Response returned to React
Enter fullscreen mode Exit fullscreen mode

This boundary keeps provider-specific behavior outside the frontend and gives the backend control over authentication headers, request construction and external API communication.

It also leaves room for future extensions.

OpenRouter could eventually become one implementation behind a generic AI provider interface without requiring significant changes to the UI.


Handling external API credentials

Allowing users to configure OpenRouter introduces a security requirement that is not present in a basic CRUD flow: third-party credentials have to be persisted without treating them as ordinary application data.

The project encrypts API keys before storing them in PostgreSQL using AES in GCM mode.

The backend decrypts the value only when it needs to issue a request to the external provider.

Conceptually:

OpenRouter API key
       |
       v
AES-GCM encryption
       |
       v
Encrypted database value
       |
       v
Backend decryption
       |
       v
External API request
Enter fullscreen mode Exit fullscreen mode

Keeping this responsibility on the server also prevents provider credentials from becoming part of the React application configuration.


The React frontend

The frontend is implemented with React 19 and TypeScript and built with Vite.

The source tree is organized around a small set of responsibilities:

components/
pages/
context/
services/
types/
Enter fullscreen mode Exit fullscreen mode

React Router handles navigation, while React Context is used for the authenticated user state.

For the current size of the application, Context is sufficient. Introducing a larger state management solution would add an additional abstraction without solving a concrete problem.

Most application state remains local to the page or component that owns it, while only genuinely shared state is lifted into the authentication context.


A dedicated API layer

HTTP communication is centralized in a dedicated frontend service instead of being spread across individual components.

That layer exposes operations for:

  • authentication;
  • prompt CRUD operations;
  • AI configuration;
  • chat requests;
  • administrative user management.

This keeps React components focused on rendering and interaction while request construction, credentials and common response handling remain in one place.

It also creates a useful boundary if the backend API changes later: most HTTP-level changes can be isolated inside the service layer rather than propagated across the entire UI.


The chat flow

The chat page is where most parts of the system converge.

A user can work with a saved prompt, provide additional context and send a conversation to the configured language model.

React manages the interaction and conversation state, Spring Boot performs the authenticated server-side operation, PostgreSQL provides the user-specific configuration, and OpenRouter handles the model request.

The resulting flow is:

Prompt
  +
User message
  |
  v
React chat interface
  |
  v
Spring Boot
  |
  v
OpenRouter
  |
  v
AI model response
  |
  v
React chat interface
Enter fullscreen mode Exit fullscreen mode

This is probably the part of the project that best represents the overall architecture because it crosses every major boundary of the application without coupling those layers together.


Why I kept the architecture simple

One of the main design decisions behind AI Prompt Lab was to avoid solving problems the application does not have.

The backend is a single Spring Boot application because there is currently no domain or operational requirement that would justify distributing the system across multiple services.

The frontend does not use Redux because authentication is the only significant global state and React Context already covers that requirement.

The data model remains relational because the application's entities and ownership relationships map naturally to PostgreSQL.

This is a principle I tend to apply regardless of the technology stack: introduce abstraction when it removes meaningful complexity, not simply because the abstraction exists.

A relatively small system with explicit boundaries is often easier to evolve than an over-engineered system whose infrastructure is more complex than its domain.


Possible extensions

The current architecture leaves several natural directions for future development.

Prompt categories and tags

Prompts could be grouped by domain, purpose or workflow, making larger collections easier to manage.

Prompt versioning

Instead of replacing prompt content on every update, previous versions could be preserved and compared.

Streaming responses

The chat API could move from request-response communication to Server-Sent Events or another streaming mechanism so generated content appears progressively in the UI.

Multiple AI providers

An internal provider abstraction could support OpenRouter, OpenAI, Anthropic or locally hosted models behind a common application interface.

Conversation persistence

Chat sessions could be stored and reopened instead of existing only for the duration of the current frontend session.

Usage metrics

The backend could collect token usage, request latency and estimated cost per model, turning the application into a more complete prompt experimentation environment.


Conclusion

AI Prompt Lab is a compact full-stack application that brings together several concerns that usually appear in real-world systems: persistence, authentication, authorization, secret handling, external HTTP integrations and a typed frontend.

The backend uses Java 21 and Spring Boot to provide the application and security layer, PostgreSQL manages persistent state, React 19 and TypeScript provide the user interface, and OpenRouter connects the application to different language models.

The project is intentionally not built around architectural novelty.

Its design is based on keeping responsibilities explicit and introducing complexity only where the requirements justify it.

For me, that is what makes the project interesting: frontend, backend, persistence, security and AI integration are treated as parts of the same system, while each layer remains responsible for a clearly defined concern.


Source Code

The complete source code is available on GitHub:

Java21_React19_AIPromptLab on GitHub


Originally published on CertosinoLab.

Top comments (0)