DEV Community

Cover image for From 20 Lines to 4: My First AI Endpoint in Spring Boot
Sham Prakash K
Sham Prakash K

Posted on AI-assisted

From 20 Lines to 4: My First AI Endpoint in Spring Boot

Before you install any framework, before you add any dependency — you should make one raw HTTP call to the model yourself. Just to see what it actually is.

That's how I started. And it's how I'd recommend everyone starts.

Step 1 — Get a Gemini API key

Go to Google AI Studio and create a free API key. No credit card needed. Gemini has a generous free tier — more than enough to learn and build.

Once you have the key, store it as an environment variable. Never paste it directly into your code.

GEMINI_API_KEY=your_key_here
Enter fullscreen mode Exit fullscreen mode

Step 2 — Understand what you're calling

Gemini exposes a REST API. The endpoint looks like this:

POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=YOUR_API_KEY
Enter fullscreen mode Exit fullscreen mode

You send a JSON body, you get a JSON response. That's it. No magic.

The request body looks like this:

{
  "contents": [
    {
      "role": "user",
      "parts": [{ "text": "What is a token in LLMs?" }]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

And the response comes back like this:

{
  "candidates": [
    {
      "content": {
        "parts": [{ "text": "A token is a piece of text..." }],
        "role": "model"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Your answer is at candidates[0].content.parts[0].text. Navigate that JSON tree and you have your response.


Step 3 — Build it with plain Java, no frameworks

Here's a working Spring Boot controller that calls Gemini using nothing but Java's built-in HttpClient and Jackson (which Spring Boot already includes). No Spring AI. No extra dependencies.

pom.xml — intentionally minimal:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

That's the only dependency. Spring Boot 3.x includes Jackson. Java 11+ includes HttpClient. Nothing else needed.

application.properties:

gemini.api.key=${GEMINI_API_KEY}
gemini.api.url=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
Enter fullscreen mode Exit fullscreen mode

ChatController.java:

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    @Value("${gemini.api.key}")
    private String apiKey;

    @Value("${gemini.api.url}")
    private String apiUrl;

    private final ObjectMapper objectMapper = new ObjectMapper();
    private final HttpClient httpClient = HttpClient.newHttpClient();

    @PostMapping
    public ResponseEntity<String> chat(@RequestBody String userMessage) throws Exception {

        // Build the request body
        String requestBody = """
            {
              "contents": [
                {
                  "role": "user",
                  "parts": [{ "text": "%s" }]
                }
              ]
            }
        """.formatted(escape(userMessage));

        // Make the HTTP call
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(apiUrl + "?key=" + apiKey))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(requestBody))
            .build();

        HttpResponse<String> response = httpClient.send(request,
            HttpResponse.BodyHandlers.ofString());

        // Parse the response — answer is at candidates[0].content.parts[0].text
        JsonNode root = objectMapper.readTree(response.body());

        if (root.has("error")) {
            return ResponseEntity.status(500)
                .body(root.path("error").path("message").asText());
        }

        String reply = root
            .path("candidates").get(0)
            .path("content")
            .path("parts").get(0)
            .path("text").asText();

        return ResponseEntity.ok(reply);
    }

    private String escape(String text) {
        return text
            .replace("\\", "\\\\")
            .replace("\"", "\\\"")
            .replace("\n", "\\n")
            .replace("\r", "\\r");
    }
}
Enter fullscreen mode Exit fullscreen mode

Start the app, send a POST request to /api/chat with a message body, and you'll get a response from Gemini.

That's a fully working AI chat endpoint. No AI framework. Just HTTP.

Full source code for this phase: github.com/shamprakash2000/gemini-chat/tree/phase-1-2-plain-http


What you just built — and what's missing

This works. Send a message, get a response. That's a real AI endpoint.

But look at what you're doing manually:

  • Building JSON by hand — string formatting, escaping characters, constructing the payload yourself
  • Parsing JSON by hand — navigating .path().get().path() to extract the answer
  • No model switching — the URL and response format are Gemini-specific. Switching to Claude means rewriting everything

For one simple call, this is fine. But the moment you want conversation memory, tool calling, streaming, or RAG — you're building a framework from scratch.

That's where Spring AI comes in.


What Spring AI actually is

Spring AI is the Spring team's answer to: "we keep writing the same boilerplate to call LLMs — let's standardise it."

It gives you:

A common interface across models. The same ChatClient code works for Gemini, OpenAI, Claude, Ollama. You swap the dependency and config — the Java code stays the same.

No more manual JSON. The framework builds the request payload and parses the response for you.

Conversation memory, tool calling, structured output, streaming — all built in. We'll get to each of these in the articles ahead. For now, just understand what the foundation gives you.


The same endpoint, rewritten with Spring AI

Add the dependency:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.1.8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-google-gemini</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Update application.properties:

spring.ai.google.gemini.api-key=${GEMINI_API_KEY}
spring.ai.google.gemini.chat.options.model=gemini-2.5-flash
Enter fullscreen mode Exit fullscreen mode

Now the controller:

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultSystem("You are a helpful assistant.")
            .build();
    }

    @PostMapping
    public String chat(@RequestBody String userMessage) {
        return chatClient.prompt()
            .user(userMessage)
            .call()
            .content();
    }
}
Enter fullscreen mode Exit fullscreen mode

Same functionality. A fraction of the code.


What Spring AI does not protect you from

I want to be honest here because most tutorials aren't.

The API changes between minor versions.

I was on Spring AI 1.0.x and upgraded to 1.1.x. Two things broke silently — no compiler errors, no deprecation warnings. Just runtime failures. The ChatClient builder API had changed, and the way conversation memory was configured had moved to a different class entirely.

Lesson: when you upgrade Spring AI, read the full changelog and test every integration point. Don't assume a minor version bump is safe.

The abstraction hides internals.

The plain HTTP version showed you exactly what was happening — every byte in, every byte out. Spring AI hides that. When something goes wrong deep in the framework, you need to know what's underneath to debug it. That's another reason to start with plain HTTP first — so you have that mental model when the abstraction breaks.


Plain HTTP vs Spring AI — the honest comparison

Plain HTTP Spring AI
Dependencies None beyond Spring Web Spring AI starter
Code volume High Low
JSON handling Manual Automatic
Model switching Full rewrite Swap config
Tool calling / agents Build from scratch Built in
Debugging Full visibility Abstraction hides internals
Version stability Stable (it's just HTTP) Breaking changes between minors
Good for Learning, full control Production Spring Boot apps

Start with plain HTTP. Understand what you're abstracting. Then move to Spring AI.


What's next

Spring AI is set up and working. Next: building a proper chat application — with persistent conversation history in PostgreSQL, session management, and a system prompt that shapes the model's behaviour.


Did you hit a breaking change in Spring AI between versions? Drop it in the comments — you're not the only one.

Sham Prakash K — Backend Engineer, 4+ years in Java, Spring Boot, and distributed systems. Building AI backend infrastructure. Writing about what I actually learned, mistakes included.

Top comments (0)