A client wanted a chatbot. Not a fancy one, just something that could answer basic support questions using their knowledge base. “Can you wire up ChatGPT?” they asked. I’d never done it before, but I figured Spring Boot + REST call couldn’t be that hard. I was right. The whole thing took an afternoon, and most of that was me arguing with my own config files.
Here’s how to do it properly, including the stuff that tripped me up.
Prerequisites
Before we start coding, make sure you have:
- A Spring Boot project (Java 17 or higher recommended).
- An OpenAI API Key (you can get one from the OpenAI Dashboard).
Setting Up Dependencies
You only need the standard Spring Boot Web starter. If you’re on Spring Boot 3.2+, you get RestClient built in, no extra libraries required.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
That’s it. Seriously. No Spring AI, no special OpenAI SDK. Just a REST call. (I spent twenty minutes looking for an openai-spring-boot-starter before I realized I didn’t need one.)
Configuring the API Key
Store your OpenAI API key in src/main/resources/application.properties. Or use environment variables for better security, I’d recommend the latter.
openai.api.key=your_api_key_here
openai.api.url=https://api.openai.com/v1/chat/completions
I hardcoded mine for a quick test and then had to do the git-filter-fruit to remove it from my history. Don’t be like me.
Building the Request and Response Models
OpenAI’s Chat Completion API expects a specific JSON structure. We need DTOs that match.
ChatRequest.java
public record ChatRequest(
String model,
List<Message> messages
) {
public record Message(String role, String content) {}
}
ChatResponse.java
public record ChatResponse(
List<Choice> choices
) {
public record Choice(Message message) {}
public record Message(String role, String content) {}
}
Records work perfectly here, small, immutable, and the JSON deserialization just works. I’ve seen people use Lombok for this but honestly records are cleaner.
The AI Service
Now the actual service that talks to OpenAI. RestClient (introduced in Spring 6) gives us a nice fluent API:
@Service
public class ChatGptService {
private final RestClient restClient;
@Value("${openai.api.key}")
private String apiKey;
@Value("${openai.api.url}")
private String apiUrl;
public ChatGptService(RestClient.Builder builder) {
this.restClient = builder.build();
}
public String getChatResponse(String prompt) {
ChatRequest request = new ChatRequest(
"gpt-3.5-turbo",
List.of(new ChatRequest.Message("user", prompt))
);
ChatResponse response = restClient.post()
.uri(apiUrl)
.header("Authorization", "Bearer " + apiKey)
.body(request)
.retrieve()
.body(ChatResponse.class);
return response.choices().get(0).message().content();
}
}
A few things to note: RestClient.Builder is injected by Spring, which is why we don’t call RestClient.create() ourselves. And gpt-3.5-turbo was the model the client wanted, swap it for gpt-4o if you need the smarter (and more expensive) model.
I did hit a weird issue the first time: the response body was coming back as null. Turns out I’d forgotten the Authorization header. The error message from OpenAI was vague enough that it took me a while to figure out.
The Controller
Expose an endpoint so the outside world can talk to it:
@RestController
@RequestMapping("/api/ai")
public class AIController {
private final ChatGptService chatGptService;
public AIController(ChatGptService chatGptService) {
this.chatGptService = chatGptService;
}
@GetMapping("/chat")
public String chat(@RequestParam String prompt) {
return chatGptService.getChatResponse(prompt);
}
}
Quick and dirty. For production, I’d add request validation, rate limiting, and probably a POST endpoint instead of GET (query strings have length limits, and some prompts get long).
Testing It
Run your Spring Boot app and hit the endpoint:
curl "http://localhost:8080/api/ai/chat?prompt=Tell+me+a+joke+about+Java"
When I first saw a response come back from OpenAI through my own Spring Boot service, it felt like I’d connected to the internet’s brain. (I know it’s just an HTTP POST under the hood, but let me have my moment.)
Things I Learned the Hard Way
Error handling matters. OpenAI will rate-limit you, they’ll go down occasionally: and sometimes the response will just be weird. I’d suggest wrapping the RestClient call in a try-catch at minimum: and Resilience4j’s @Retryable if you want to be thorough.
Streaming for long responses. If you’re expecting long outputs: a standard POST-and-wait approach means the user stares at nothing for seconds. Look into WebClient for Server-Sent Events (SSE): it’s more code but the UX is worth it.
Prompt engineering is underrated. The difference between “summarize this data” and a well-structured system message with constraints is night and day. Spend time on your prompts.
Security. Never: ever commit your API key. Use environment variables or a secrets manager. I cannot stress this enough because I learned it the expensive way.
The combination of Spring Boot’s tooling and OpenAI’s API is surprisingly straightforward. You don’t need special frameworks or complex architectures, just a REST call, some DTOs, and good error handling. The hardest part is the config, and even that’s just a few lines.
Member discussion
0 commentsStart the conversation
Become a member of >hacksubset_ to start commenting.
Already a member? Sign in