{"slug": "build-your-first-spring-ai-application-with-openai-using-spring-boot", "title": "Build Your First Spring AI Application with OpenAI Using Spring Boot", "summary": "A developer demonstrates how to build a Spring AI application using OpenAI with Spring Boot, covering project setup, configuration, and creating a REST API that sends prompts to an AI model. The tutorial walks through connecting Spring Boot to OpenAI, using the ChatClient and ChatModel abstractions, and emphasizes secure API key handling. The result is a working AI-powered Spring Boot endpoint that returns AI responses.", "body_md": "If you have been following this Spring AI series, you already understand the two most important abstractions: `ChatClient`\n\nand `ChatModel`\n\n.\n\nIn the previous article, we learned that:\n\n```\nYour Java Code\n      |\n      v\n ChatClient\n      |\n      v\n ChatModel\n      |\n      v\n AI Provider\n```\n\n`ChatClient`\n\ngives us a developer-friendly API, while `ChatModel`\n\nhandles the underlying model integration.\n\nNow it is time to build something real.\n\nIn this article, we will build our first **Spring AI application using OpenAI**.\n\nWe will start from project setup and configuration, connect Spring Boot to OpenAI, create a REST API, send prompts to an AI model, and understand what happens behind the scenes.\n\nBy the end, you will have a working AI-powered Spring Boot API.\n\nOur application will expose a simple REST endpoint:\n\n```\nGET /api/chat?message=Explain dependency injection\n```\n\nThe flow will look like this:\n\n```\nClient\n   |\n   v\nSpring Boot REST API\n   |\n   v\nChatClient\n   |\n   v\nChatModel\n   |\n   v\nOpenAI\n   |\n   v\nAI Response\n```\n\nThe goal is intentionally simple.\n\nWe want to understand the complete flow before adding more advanced concepts such as RAG, tools, memory, structured output, and AI agents.\n\nBefore starting, you should have:\n\nYou should also be comfortable with dependency injection and creating REST controllers.\n\nIf you have followed the previous articles in this series, most of this should already be familiar.\n\nThe easiest way to create the project is through Spring Initializr.\n\nChoose:\n\n```\nProject: Maven\n\nLanguage: Java\n\nSpring Boot: Your compatible Spring Boot version\n\nPackaging: Jar\n\nJava: 17 or later\n```\n\nFor dependencies, we need Spring Web and the Spring AI OpenAI starter.\n\nYour project will eventually look something like:\n\n```\nspring-ai-openai-demo\n│\n├── src\n│   ├── main\n│   │   ├── java\n│   │   │   └── com.example.demo\n│   │   │       └── DemoApplication.java\n│   │   │\n│   │   └── resources\n│   │       └── application.properties\n│\n└── pom.xml\n```\n\nAdd the Spring AI OpenAI model starter to your Maven configuration.\n\nFor example:\n\n```\n<dependency>\n    <groupId>org.springframework.ai</groupId>\n    <artifactId>spring-ai-starter-model-openai</artifactId>\n</dependency>\n```\n\nYou will also need Spring Web:\n\n```\n<dependency>\n    <groupId>org.springframework.boot</groupId>\n    <artifactId>spring-boot-starter-web</artifactId>\n</dependency>\n```\n\nYour project now has the components required to build a simple AI REST API.\n\nConceptually:\n\n```\nSpring Boot\n     |\n     +-- Spring Web\n     |\n     +-- Spring AI\n             |\n             +-- OpenAI integration\n```\n\nSpring AI needs credentials to communicate with OpenAI.\n\nYou can configure the API key using an environment variable.\n\nFor example:\n\n```\nexport OPENAI_API_KEY=your-api-key\n```\n\nOn Windows PowerShell:\n\n```\n$env:OPENAI_API_KEY=\"your-api-key\"\n```\n\nThen reference it from your Spring configuration:\n\n```\nspring.ai.openai.api-key=${OPENAI_API_KEY}\n```\n\nThe important part is that your API key should **not be hard-coded inside your Java source code**.\n\nAvoid doing this:\n\n```\nString apiKey = \"sk-xxxxxxxx\";\n```\n\nInstead, keep secrets outside your source code.\n\nA better approach is:\n\n```\nEnvironment Variable\n        |\n        v\nSpring Configuration\n        |\n        v\nSpring AI\n        |\n        v\nOpenAI\n```\n\nThis becomes even more important when deploying your application to production.\n\nSpring AI needs to know which OpenAI chat model your application should use.\n\nYou can configure the model through your application properties.\n\nFor example:\n\n```\nspring.ai.openai.chat.options.model=your-model\n```\n\nThe exact model you choose depends on the models available to your OpenAI account and the requirements of your application.\n\nThe important concept is that your application does not need to manually construct OpenAI HTTP requests.\n\nSpring AI handles that integration.\n\nNow let's create our first AI-powered controller.\n\n```\n@RestController\n@RequestMapping(\"/api\")\npublic class ChatController {\n\n    private final ChatClient chatClient;\n\n    public ChatController(ChatClient.Builder chatClientBuilder) {\n        this.chatClient = chatClientBuilder.build();\n    }\n\n    @GetMapping(\"/chat\")\n    public String chat(@RequestParam(\"message\") String message) {\n\n        return chatClient\n                .prompt(message)\n                .call()\n                .content();\n    }\n}\n```\n\nLet's understand this carefully.\n\nThe constructor receives:\n\n```\nChatClient.Builder chatClientBuilder\n```\n\nSpring AI provides this builder through Spring Boot auto-configuration when the appropriate model integration is configured.\n\nWe then create our `ChatClient`\n\n:\n\n```\nthis.chatClient = chatClientBuilder.build();\n```\n\nNow the controller has a ready-to-use AI client.\n\nThe architecture looks like:\n\n```\nSpring Boot\n     |\n     v\nChatClient.Builder\n     |\n     | build()\n     v\nChatClient\n```\n\nThis is the same concept we explored in Part 2 of this series.\n\nNow let's call the API.\n\nStart your Spring Boot application.\n\nThen send:\n\n```\nGET /api/chat?message=What is dependency injection in Spring?\n```\n\nThe controller receives:\n\n```\nWhat is dependency injection in Spring?\n```\n\nand passes it to:\n\n```\nchatClient\n        .prompt(message)\n        .call()\n        .content();\n```\n\nThe flow is:\n\n```\nHTTP Request\n     |\n     v\nChatController\n     |\n     v\nChatClient\n     |\n     v\nChatModel\n     |\n     v\nOpenAI\n     |\n     v\nAI Response\n```\n\nThe generated response is returned to the client.\n\nThat's it.\n\nYou have now built a Spring Boot application that can communicate with an AI model.\n\nLet's look at this code again:\n\n```\nchatClient\n        .prompt(message)\n        .call()\n        .content();\n```\n\nThere are three important operations here.\n\n```\n.prompt(message)\n```\n\nThis defines the prompt that you want to send to the model.\n\nFor example:\n\n```\n.prompt(\"Explain Java interfaces\")\n```\n\nor:\n\n```\n.prompt(\"Write a SQL query to find duplicate users\")\n```\n\nor:\n\n```\n.prompt(message)\n```\n\nwhere `message`\n\ncomes from an HTTP request.\n\n```\n.call()\n```\n\nThis executes the model interaction.\n\nYou can think about it as:\n\n```\nBuild Prompt\n     |\n     v\nCall Model\n.content()\n```\n\nThis extracts the generated text from the response.\n\nSo the entire chain:\n\n```\nchatClient\n        .prompt(message)\n        .call()\n        .content();\n```\n\ncan be mentally understood as:\n\n```\nCreate Prompt\n     |\n     v\nCall AI Model\n     |\n     v\nExtract Text\n```\n\nThis fluent style is one of the reasons `ChatClient`\n\nis convenient for application developers.\n\nAlthough putting the AI call directly inside a controller works for a small demonstration, it is not how I would structure a production application.\n\nInstead, let's introduce a service.\n\nOur architecture becomes:\n\n```\nClient\n  |\n  v\nController\n  |\n  v\nService\n  |\n  v\nChatClient\n  |\n  v\nOpenAI\n```\n\nCreate:\n\n```\n@Service\npublic class ChatService {\n\n    private final ChatClient chatClient;\n\n    public ChatService(ChatClient.Builder builder) {\n        this.chatClient = builder.build();\n    }\n\n    public String generateResponse(String message) {\n\n        return chatClient\n                .prompt(message)\n                .call()\n                .content();\n    }\n}\n```\n\nThen our controller becomes:\n\n```\n@RestController\n@RequestMapping(\"/api\")\npublic class ChatController {\n\n    private final ChatService chatService;\n\n    public ChatController(ChatService chatService) {\n        this.chatService = chatService;\n    }\n\n    @GetMapping(\"/chat\")\n    public String chat(@RequestParam String message) {\n\n        return chatService.generateResponse(message);\n    }\n}\n```\n\nThis separation is much cleaner.\n\nThe controller handles HTTP.\n\nThe service handles AI interaction.\n\nImagine that six months from now your application has:\n\n```\nChatController\nEmailController\nSupportController\nDocumentController\n```\n\nIf every controller directly interacts with the AI model, your code can quickly become difficult to maintain.\n\nInstead:\n\n```\nControllers\n     |\n     v\nAI Services\n     |\n     v\nChatClient\n     |\n     v\nChatModel\n```\n\nThis keeps your application organized.\n\nIt also makes it easier to add features later.\n\nSo far, we have only sent a user prompt.\n\nBut real AI applications usually need more control.\n\nFor example, imagine we are building a customer support assistant.\n\nWe don't want the model to behave like a generic chatbot.\n\nWe want to tell it:\n\n```\nYou are a customer support assistant.\nAnswer clearly.\nKeep responses concise.\nDo not invent information.\n```\n\nWe can do that with a system message.\n\nFor example:\n\n```\nreturn chatClient\n        .prompt()\n        .system(\"\"\"\n                You are a helpful customer support assistant.\n                Answer clearly and concisely.\n                Do not invent information.\n                \"\"\")\n        .user(message)\n        .call()\n        .content();\n```\n\nNow we have two different types of instructions:\n\n```\nSystem Message\n      +\nUser Message\n      |\n      v\n    Model\n```\n\nThis distinction will become extremely important later in the series.\n\nThink about the two messages like this.\n\nDefines the behavior of the assistant.\n\n```\nYou are a Java programming assistant.\nAlways provide production-quality examples.\n```\n\nContains the actual request.\n\n```\nExplain dependency injection.\n```\n\nTogether:\n\n```\nSystem\n  |\n  | \"You are a Java assistant\"\n  |\n  v\nUser\n  |\n  | \"Explain dependency injection\"\n  |\n  v\nAI Model\n```\n\nThe PDF structure for this series introduces system and user messages as dedicated upcoming topics, so we will explore them in much more detail later.\n\nLet's make our API slightly more realistic.\n\nInstead of simply passing the user's message directly to the model, we can create a dedicated service method:\n\n```\n@Service\npublic class ChatService {\n\n    private final ChatClient chatClient;\n\n    public ChatService(ChatClient.Builder builder) {\n        this.chatClient = builder.build();\n    }\n\n    public String ask(String question) {\n\n        return chatClient\n                .prompt()\n                .system(\"\"\"\n                        You are a helpful Java and Spring Boot assistant.\n                        Explain technical concepts clearly.\n                        Use examples when appropriate.\n                        \"\"\")\n                .user(question)\n                .call()\n                .content();\n    }\n}\n```\n\nThen:\n\n```\n@RestController\n@RequestMapping(\"/api/chat\")\npublic class ChatController {\n\n    private final ChatService chatService;\n\n    public ChatController(ChatService chatService) {\n        this.chatService = chatService;\n    }\n\n    @GetMapping\n    public String chat(@RequestParam String message) {\n\n        return chatService.ask(message);\n    }\n}\n```\n\nNow our API becomes:\n\n```\nGET /api/chat?message=What is Spring Boot?\n```\n\nand the service controls how the AI behaves.\n\nThis simple line:\n\n```\nchatClient\n        .prompt()\n        .user(message)\n        .call()\n        .content();\n```\n\nhides several operations.\n\nConceptually:\n\n```\n                    Spring Boot\n                         |\n                         v\n                    ChatClient\n                         |\n                         v\n                 Build Chat Request\n                         |\n                         v\n                    ChatModel\n                         |\n                         v\n                  OpenAI Integration\n                         |\n                         v\n                    OpenAI API\n                         |\n                         v\n                    AI Response\n                         |\n                         v\n                    ChatClient\n                         |\n                         v\n                       String\n```\n\nThis abstraction is one of the major benefits of Spring AI.\n\nYou focus on your application.\n\nSpring AI handles the model integration.\n\nThis is another reason abstractions are useful.\n\nWithout Spring AI, you might build:\n\n```\nSpring Boot\n     |\n     v\nCustom HTTP Client\n     |\n     v\nOpenAI API\n```\n\nYour application would now contain provider-specific request and response handling.\n\nWith Spring AI:\n\n```\nSpring Boot\n     |\n     v\nChatClient\n     |\n     v\nChatModel\n     |\n     v\nProvider\n```\n\nYour application code stays focused on the AI interaction rather than provider-specific implementation details.\n\nThis is the abstraction we discussed in the previous article.\n\nIf you're a Spring Boot developer, think about Spring AI in layers.\n\n```\nApplication Layer\n       |\n       v\n   ChatClient\n       |\n       v\n    ChatModel\n       |\n       v\n   AI Provider\n       |\n       v\n    AI Model\n```\n\nEach layer has a responsibility.\n\nBusiness logic.\n\nDeveloper-friendly AI interaction.\n\nModel/provider abstraction.\n\nOpenAI, Ollama, AWS Bedrock, and other supported providers.\n\nThe actual language model generating the response.\n\nOne of the biggest mistakes beginners make is committing API keys to Git.\n\nNever do this:\n\n```\nspring.ai.openai.api-key=sk-your-secret-key\n```\n\ninside a repository that will be shared publicly.\n\nInstead:\n\n```\nspring.ai.openai.api-key=${OPENAI_API_KEY}\n```\n\nand configure the environment variable separately.\n\nFor local development:\n\n```\nOPENAI_API_KEY\n```\n\nFor production, use your deployment platform's secret management mechanism.\n\nThe principle is simple:\n\n```\nSource Code\n     X\n     |\n     | No secrets\n     |\nEnvironment / Secret Store\n     |\n     v\nSpring Boot\n```\n\nNever commit secrets.\n\nIf your key is exposed, rotate it immediately.\n\nAvoid this structure:\n\n``` php\nController 1 -> AI\nController 2 -> AI\nController 3 -> AI\nController 4 -> AI\n```\n\nPrefer:\n\n```\nControllers\n     |\n     v\nServices\n     |\n     v\nChatClient\n```\n\nRemember:\n\n```\nChatClient != AI Model\n```\n\n`ChatClient`\n\nis the application-facing abstraction.\n\nThe underlying model integration is handled through `ChatModel`\n\n.\n\nA production AI application usually needs some control over model behavior.\n\nInstead of:\n\n```\n.prompt(message)\n```\n\nyou will often evolve toward:\n\n```\n.prompt()\n.system(\"...\")\n.user(message)\n```\n\nLater, we will see how prompt templates, advisors, memory, RAG, and tools make this even more powerful.\n\nYou don't need this on day one:\n\n```\nRAG\n +\nVector Database\n +\nTools\n +\nMCP\n +\nMemory\n +\nAgents\n +\nMultiple Models\n```\n\nStart with:\n\n```\nSpring Boot\n     |\n     v\nChatClient\n     |\n     v\nOpenAI\n```\n\nThen add complexity when your application actually needs it.\n\nOur application currently looks simple:\n\n```\nClient\n   |\n   v\nREST API\n   |\n   v\nChatClient\n   |\n   v\nOpenAI\n```\n\nBut this architecture can grow.\n\nFor example:\n\n```\n                    Spring Boot\n                         |\n                         v\n                    ChatClient\n                         |\n          +--------------+--------------+\n          |              |              |\n          v              v              v\n        Memory          RAG           Tools\n          |              |              |\n          v              v              v\n       History      Vector Store    Backend APIs\n```\n\nThis is where Spring AI becomes much more interesting.\n\nA simple chatbot can eventually become a complete AI backend.\n\nHere is a simple production-style starting point.\n\n```\n@Service\npublic class ChatService {\n\n    private final ChatClient chatClient;\n\n    public ChatService(ChatClient.Builder builder) {\n        this.chatClient = builder.build();\n    }\n\n    public String ask(String question) {\n\n        return chatClient\n                .prompt()\n                .system(\"\"\"\n                        You are a helpful Java and Spring Boot assistant.\n                        Explain concepts clearly and provide examples when useful.\n                        \"\"\")\n                .user(question)\n                .call()\n                .content();\n    }\n}\n@RestController\n@RequestMapping(\"/api/chat\")\npublic class ChatController {\n\n    private final ChatService chatService;\n\n    public ChatController(ChatService chatService) {\n        this.chatService = chatService;\n    }\n\n    @GetMapping\n    public String chat(@RequestParam String message) {\n\n        return chatService.ask(message);\n    }\n}\nspring.ai.openai.api-key=${OPENAI_API_KEY}\n\nspring.ai.openai.chat.options.model=your-model\n```\n\nThe result is a clean architecture:\n\n```\nHTTP Client\n    |\n    v\nChatController\n    |\n    v\nChatService\n    |\n    v\nChatClient\n    |\n    v\nChatModel\n    |\n    v\nOpenAI\n```\n\nIn this article, we built our first Spring AI application using OpenAI.\n\nWe learned how to:\n\n`ChatClient.Builder`\n\n`ChatClient`\n\nMost importantly, you now understand the complete request flow:\n\n```\nClient\n   |\n   v\nSpring Boot\n   |\n   v\nController\n   |\n   v\nService\n   |\n   v\nChatClient\n   |\n   v\nChatModel\n   |\n   v\nOpenAI\n```\n\nThis is the foundation for everything we will build later.\n\nA hosted AI model is useful, but what if you want to run an LLM locally?\n\nMaybe you don't want to send your data to an external provider.\n\nMaybe you want to experiment without paying for API usage.\n\nMaybe you're building an application where local inference is important.\n\nThat's where **Ollama** comes in.\n\nIn the next article, we will explore:\n\n**Part 4: Run Local LLMs with Ollama and Spring AI**\n\nWe will install Ollama, run a local model, connect it to Spring AI, and see how the same `ChatClient`\n\napplication can work with a local LLM.\n\nThat is one of the most interesting parts of Spring AI:\n\n``` php\nSame Application\n       |\n       +------> OpenAI\n       |\n       +------> Ollama\n       |\n       +------> Other Providers\n```\n\nThe application stays focused on the AI interaction while Spring AI handles the underlying model integration.\n\nSpring AI is an abstraction layer that makes it easier for Spring applications to integrate with AI models and AI-related capabilities.\n\nYes. Spring AI provides an OpenAI integration that allows Spring Boot applications to communicate with OpenAI models.\n\nDo not hard-code it in your source code. Use environment variables or a proper secret-management solution.\n\nFor most application-level use cases, you can work primarily with `ChatClient`\n\n. `ChatModel`\n\nremains important because it represents the underlying model integration.\n\nOne of the goals of Spring AI's abstraction model is to reduce application coupling to provider-specific implementation details.\n\nThe next step in this series is running a local LLM with Ollama and Spring AI.\n\n**Part 1:** Spring AI Tutorial: How Java Developers Can Build Generative AI Applications with Spring Boot\n\n**Part 2:** ChatModel vs ChatClient in Spring AI\n\n**Part 3:** Build Your First Spring AI Application with OpenAI\n\n**Part 4:** Run Local LLMs with Ollama and Spring AI\n\n**Part 5:** Run AI Models Locally with Docker Model Runner\n\n**Part 6:** Using AWS Bedrock with Spring AI\n\n**Part 7:** Working with Multiple Chat Models in Spring AI\n\n**Part 8:** Understanding Message Roles in LLMs\n\n**Part 9:** System Messages and User Messages in Spring AI\n\n**Part 10:** Configuring Default Behavior in ChatClient\n\nThe series will continue into prompt templates, advisors, structured output, tokens, embeddings, chat memory, RAG, vector stores, tool calling, MCP, evaluation, observability, and AI agents.", "url": "https://wpnews.pro/news/build-your-first-spring-ai-application-with-openai-using-spring-boot", "canonical_source": "https://dev.to/ayshriv/build-your-first-spring-ai-application-with-openai-using-spring-boot-c3b", "published_at": "2026-08-17 19:44:38+00:00", "updated_at": "2026-08-17 20:13:52.265690+00:00", "lang": "en", "topics": ["artificial-intelligence", "developer-tools"], "entities": ["Spring AI", "OpenAI", "Spring Boot", "ChatClient", "ChatModel", "Spring Initializr"], "alternates": {"html": "https://wpnews.pro/news/build-your-first-spring-ai-application-with-openai-using-spring-boot", "markdown": "https://wpnews.pro/news/build-your-first-spring-ai-application-with-openai-using-spring-boot.md", "text": "https://wpnews.pro/news/build-your-first-spring-ai-application-with-openai-using-spring-boot.txt", "jsonld": "https://wpnews.pro/news/build-your-first-spring-ai-application-with-openai-using-spring-boot.jsonld"}}