{"slug": "building-ai-prompt-lab-with-java-21-spring-boot-and-react-19", "title": "Building AI Prompt Lab with Java 21, Spring Boot and React 19", "summary": "A developer built AI Prompt Lab, a full-stack application using Java 21, Spring Boot, React 19, TypeScript, PostgreSQL, and OpenRouter, to manage reusable AI prompts as structured assets. The application features authenticated workspaces, role-based authorization, and integration with OpenRouter for accessing various language models. The architecture is a modular monolith with a conventional layered backend, emphasizing simplicity and clear separation of concerns.", "body_md": "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.\n\nThe 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.\n\nI 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.\n\nAI Prompt Lab provides an authenticated workspace where users can create, update, organize and reuse prompts for generative AI models.\n\nEach user has an independent prompt collection and can configure an OpenRouter account to interact with different language models through the application.\n\nAt a high level, the application provides:\n\nFrom 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.\n\nThe repository is divided into two independent applications:\n\n```\nbackend/\nfrontend/\n```\n\nThe stack is intentionally conventional.\n\nThere is no microservice decomposition, no external state management library on the frontend and no additional infrastructure that the application does not currently require.\n\nFor this scope, keeping the system as a modular monolith provides a much better balance between maintainability, deployment complexity and development speed.\n\nThe Spring Boot application follows a traditional layered structure:\n\n```\ncontroller\nservice\nrepository\nmodel\nconfig\n```\n\nThe separation is straightforward.\n\nControllers define the HTTP API, services contain application logic, repositories encapsulate persistence, and configuration components handle cross-cutting concerns such as security and encryption.\n\nI 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.\n\nPrompt management is the central domain of the application.\n\nAuthenticated users can create, edit, delete and retrieve prompts associated with their account.\n\nThe 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.\n\nThe result is a small REST API with predictable resource-oriented operations and a clean separation between client-side presentation and server-side data access.\n\nAuthentication is handled entirely by Spring Security.\n\nPasswords are persisted using BCrypt hashing, while authenticated sessions are represented by a server-issued token stored in an HTTP-only cookie.\n\nThe 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.\n\nThe flow is essentially:\n\n```\nLogin request\n      |\n      v\nCredential verification\n      |\n      v\nSession token creation\n      |\n      v\nHTTP-only cookie\n      |\n      v\nSpring Security filter\n      |\n      v\nAuthenticated request\n```\n\nAuthorization is enforced on the backend rather than being delegated to the user interface.\n\nThe application distinguishes between regular users and administrators, and administrative endpoints are protected through Spring Security.\n\nThe frontend can therefore use roles to adapt navigation and presentation, while the server remains the authoritative boundary for access control.\n\nPostgreSQL provides the persistence layer, with Spring Data JPA handling repository access and entity mapping.\n\nSchema evolution is managed through Flyway migrations.\n\nI prefer keeping database changes explicit and versioned alongside the application rather than depending on automatic schema mutation at runtime.\n\nThis gives the database the same kind of traceability as the rest of the codebase:\n\n```\nV1__init_database.sql\nV2__future_change.sql\nV3__another_change.sql\n```\n\nIt 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.\n\nAI requests are handled by the backend rather than being sent directly from React to the external provider.\n\nThe 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.\n\n```\nReact\n  |\n  v\nSpring Boot API\n  |\n  v\nUser AI configuration\n  |\n  v\nOpenRouter\n  |\n  v\nLanguage model\n  |\n  v\nResponse returned to React\n```\n\nThis boundary keeps provider-specific behavior outside the frontend and gives the backend control over authentication headers, request construction and external API communication.\n\nIt also leaves room for future extensions.\n\nOpenRouter could eventually become one implementation behind a generic AI provider interface without requiring significant changes to the UI.\n\nAllowing 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.\n\nThe project encrypts API keys before storing them in PostgreSQL using AES in GCM mode.\n\nThe backend decrypts the value only when it needs to issue a request to the external provider.\n\nConceptually:\n\n```\nOpenRouter API key\n       |\n       v\nAES-GCM encryption\n       |\n       v\nEncrypted database value\n       |\n       v\nBackend decryption\n       |\n       v\nExternal API request\n```\n\nKeeping this responsibility on the server also prevents provider credentials from becoming part of the React application configuration.\n\nThe frontend is implemented with React 19 and TypeScript and built with Vite.\n\nThe source tree is organized around a small set of responsibilities:\n\n```\ncomponents/\npages/\ncontext/\nservices/\ntypes/\n```\n\nReact Router handles navigation, while React Context is used for the authenticated user state.\n\nFor 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.\n\nMost application state remains local to the page or component that owns it, while only genuinely shared state is lifted into the authentication context.\n\nHTTP communication is centralized in a dedicated frontend service instead of being spread across individual components.\n\nThat layer exposes operations for:\n\nThis keeps React components focused on rendering and interaction while request construction, credentials and common response handling remain in one place.\n\nIt 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.\n\nThe chat page is where most parts of the system converge.\n\nA user can work with a saved prompt, provide additional context and send a conversation to the configured language model.\n\nReact 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.\n\nThe resulting flow is:\n\n```\nPrompt\n  +\nUser message\n  |\n  v\nReact chat interface\n  |\n  v\nSpring Boot\n  |\n  v\nOpenRouter\n  |\n  v\nAI model response\n  |\n  v\nReact chat interface\n```\n\nThis 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.\n\nOne of the main design decisions behind AI Prompt Lab was to avoid solving problems the application does not have.\n\nThe 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.\n\nThe frontend does not use Redux because authentication is the only significant global state and React Context already covers that requirement.\n\nThe data model remains relational because the application's entities and ownership relationships map naturally to PostgreSQL.\n\nThis 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.**\n\nA relatively small system with explicit boundaries is often easier to evolve than an over-engineered system whose infrastructure is more complex than its domain.\n\nThe current architecture leaves several natural directions for future development.\n\nPrompts could be grouped by domain, purpose or workflow, making larger collections easier to manage.\n\nInstead of replacing prompt content on every update, previous versions could be preserved and compared.\n\nThe chat API could move from request-response communication to Server-Sent Events or another streaming mechanism so generated content appears progressively in the UI.\n\nAn internal provider abstraction could support OpenRouter, OpenAI, Anthropic or locally hosted models behind a common application interface.\n\nChat sessions could be stored and reopened instead of existing only for the duration of the current frontend session.\n\nThe backend could collect token usage, request latency and estimated cost per model, turning the application into a more complete prompt experimentation environment.\n\nAI 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.\n\nThe 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.\n\nThe project is intentionally not built around architectural novelty.\n\nIts design is based on keeping responsibilities explicit and introducing complexity only where the requirements justify it.\n\nFor 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.\n\nThe complete source code is available on GitHub:\n\n[Java21_React19_AIPromptLab on GitHub](https://github.com/sfestacatenate/Java21_React19_AIPromptLab)\n\n*Originally published on CertosinoLab.*", "url": "https://wpnews.pro/news/building-ai-prompt-lab-with-java-21-spring-boot-and-react-19", "canonical_source": "https://dev.to/certosinolab/building-ai-prompt-lab-with-java-21-spring-boot-and-react-19-46p8", "published_at": "2026-08-16 23:20:00+00:00", "updated_at": "2026-08-16 23:41:51.274500+00:00", "lang": "en", "topics": ["developer-tools", "ai-products", "generative-ai"], "entities": ["Java 21", "Spring Boot", "React 19", "TypeScript", "PostgreSQL", "OpenRouter", "Spring Security", "Spring Data JPA"], "alternates": {"html": "https://wpnews.pro/news/building-ai-prompt-lab-with-java-21-spring-boot-and-react-19", "markdown": "https://wpnews.pro/news/building-ai-prompt-lab-with-java-21-spring-boot-and-react-19.md", "text": "https://wpnews.pro/news/building-ai-prompt-lab-with-java-21-spring-boot-and-react-19.txt", "jsonld": "https://wpnews.pro/news/building-ai-prompt-lab-with-java-21-spring-boot-and-react-19.jsonld"}}