Skip to main content
Dat 3. semester
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

LLM API Exercise

Exercise: Build Your First LLM-Powered Java Application with Gemini

Learning goals

By the end of the exercise, you should be able to:

  • call an LLM API from Java using HttpClient,
  • send a prompt to an LLM,
  • inspect the JSON returned by an LLM API,
  • represent an API response using Java DTOs,
  • use Jackson and ObjectMapper to convert JSON,
  • request structured JSON from an LLM,
  • convert generated JSON into Java objects,
  • validate generated data before using it.

Preparation Gemini API

1. Open Google AI Studio

Go to:

https://aistudio.google.com/

Sign in with a Google account.

Google AI Studio lets you experiment with Gemini models and create an API key for the Gemini Developer API.

2. Create an API key

In Google AI Studio, choose Get API key and create a Gemini API key.

You can also start here:

https://aistudio.google.com/app/apikey

Copy the key and keep it private.

An API key is a secret.

Do not:

- put it directly in Java source code,
- commit it to GitHub,
- send it to other students,
- include it in screenshots.

If you accidentally expose an API key, revoke it and create another one.

3. Free-tier note

Gemini Developer API provides a free tier for supported models, subject to Google’s current rate limits.

For this classroom exercise, use the model specified by your teacher.

Suggested model:

gemini-3.5-flash-lite

This model currently has free-tier access and is more than sufficient for this exercise.

Free-tier availability, model names, and rate limits can change. Follow your teacher’s model choice if it differs.

Also remember: data submitted using the free tier may be used by Google to improve its products. Use only classroom/example data. Do not submit confidential, company, or personal information.

4. Store the API key in an environment variable

Use:

GEMINI_API_KEY

macOS / Linux

export GEMINI_API_KEY="your-api-key-here"

Windows PowerShell

$env:GEMINI_API_KEY="your-api-key-here"

If you use IntelliJ, you can alternatively add GEMINI_API_KEY to the environment variables in your application’s Run Configuration.

5. Read the key in Java

String apiKey = System.getenv("GEMINI_API_KEY");

if (apiKey == null || apiKey.isBlank()) {
    throw new IllegalStateException(
        "GEMINI_API_KEY is not configured"
    );
}

Do not do this:

String apiKey = "AIza...";

6. The Gemini REST endpoint

Gemini exposes a REST API.

For this exercise, the basic endpoint has this form:

https://generativelanguage.googleapis.com/v1beta/models/MODEL:generateContent

Using our model:

https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash-lite:generateContent

We will send the API key in the x-goog-api-key HTTP header.

7. What does a request look like?

A simple request body looks like this:

{
  "contents": [
    {
      "parts": [
        {
          "text": "Explain what a REST API is in three sentences."
        }
      ]
    }
  ]
}

Notice the nested data structure:

contents
  |
  +-- parts
        |
        +-- text

This should already look familiar if you have previously worked with JSON APIs.

8. Create the request body using Jackson

Instead of manually building JSON strings, we can use Java data structures and Jackson:

ObjectMapper mapper = new ObjectMapper();

Map<String, Object> body = Map.of(
    "contents", List.of(
        Map.of(
            "parts", List.of(
                Map.of(
                    "text",
                    "Explain what a REST API is in three sentences."
                )
            )
        )
    )
);

String jsonBody = mapper.writeValueAsString(body);

You will need:

import com.fasterxml.jackson.databind.ObjectMapper;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;

9. Create the HTTP request

String model = "gemini-3.5-flash-lite";

String endpoint =
    "https://generativelanguage.googleapis.com/v1beta/models/"
    + model
    + ":generateContent";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .header("x-goog-api-key", apiKey)
    .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
    .build();

10. Send the request

HttpClient client = HttpClient.newHttpClient();

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

System.out.println("HTTP status: " + response.statusCode());
System.out.println(response.body());

A successful request should normally return:

HTTP status: 200

followed by a JSON response.

Do not worry about extracting the answer yet.

That is part of the exercise.

Setup checkpoint

Before starting the timed exercise, verify that:

  • your Java application runs,
  • Jackson is available,
  • Java can read GEMINI_API_KEY,
  • you know which Gemini model to use,
  • the request returns HTTP status 200,
  • you can see the raw JSON response.

Part 1 — Make an LLM request (0–10 min)

Start with this prompt:

Explain what a REST API is in three sentences.

Send it to Gemini and print the complete response.

Your program now has this basic architecture:

Java application
       |
       | HTTP POST
       v
  Gemini API
       |
       | JSON
       v
Java application

Questions

Look at the returned JSON.

  1. Is the generated answer the complete HTTP response?
  2. Where in the JSON is the generated text?
  3. What arrays and objects can you identify?
  4. What other information does Gemini return?
  5. What HTTP status code did you receive?

Part 2 — Experiment with prompts (10–20 min)

An LLM generates its response based on the input it receives.

Try these prompts one at a time.

Prompt A

Explain recursion.

Prompt B

Explain recursion to a 10-year-old.

Prompt C

Explain recursion to a third-semester computer science student.
Use Java in your explanation.

Prompt D

Explain recursion in exactly three sentences.
Then provide one small Java example.

Compare the responses.

Questions

  1. How much does changing the prompt change the answer?
  2. Which prompt produced the most useful answer?
  3. What information made the response more useful?
  4. Do you think the same prompt will always produce exactly the same answer?

Small challenge

Write a prompt that asks Gemini to explain:

Dependency Injection

The explanation should be aimed at a first-semester computer science student and contain a small Java example.


Part 3 — Convert the Gemini response to DTOs (20–35 min)

So far, your application prints the entire JSON response.

That is not particularly useful.

We want something like:

String answer = ...;

System.out.println(answer);

Task 1 — Inspect the response

Find the generated text in the JSON.

The response contains nested structures conceptually similar to:

response
  |
  +-- candidates[]
        |
        +-- content
              |
              +-- parts[]
                    |
                    +-- text

Task 2 — Create DTOs

Create Java records representing the relevant parts of the Gemini response.

You will need to represent:

GeminiResponse
    |
    +-- List<Candidate>

Candidate
    |
    +-- Content

Content
    |
    +-- List<Part>

Part
    |
    +-- text

For example:

public record Part(
    String text
) {
}

Create the remaining records yourself.

Task 3 — Use ObjectMapper

Convert the response:

ObjectMapper mapper = new ObjectMapper();

GeminiResponse geminiResponse =
    mapper.readValue(
        response.body(),
        GeminiResponse.class
    );

Then navigate through your DTOs and print only the generated text.

The goal is:

HTTP response
      |
      v
     JSON
      |
      v
 ObjectMapper
      |
      v
GeminiResponse
      |
      v
 generated text

Questions

  1. How is this similar to consuming any other REST API?
  2. What happens if candidates is empty?
  3. Should your program assume that every response contains generated text?

Part 4 — Generate structured data (35–50 min)

Displaying generated text is useful, but an application often needs data, not prose.

We are going to ask Gemini to create a quiz question.

Task 1 — Create a QuizQuestion DTO

public record QuizQuestion(
    String question,
    List<String> answers,
    int correctAnswer
) {
}

We want Gemini to produce data with this structure:

{
  "question": "What does HTTP status 404 mean?",
  "answers": [
    "Unauthorized",
    "Not Found",
    "Internal Server Error",
    "Created"
  ],
  "correctAnswer": 1
}

correctAnswer is the zero-based index of the correct answer.

Task 2 — Start with prompt instructions

Try asking:

Generate one multiple-choice question about Java.

The question must have exactly four possible answers.

Return only JSON with these properties:

question
answers
correctAnswer

correctAnswer must be the zero-based index of the correct answer.

Do not include Markdown or explanations.

Run it several times.

Does the model always produce exactly what you expect?

Task 3 — Request JSON output

Gemini supports structured JSON output.

Add a generationConfig to the request:

{
  "contents": [
    {
      "parts": [
        {
          "text": "Generate one multiple-choice question about Java."
        }
      ]
    }
  ],
  "generationConfig": {
    "responseMimeType": "application/json"
  }
}

This tells Gemini that the generated response should be JSON.

For production applications, Gemini can also be given a JSON schema describing the exact required structure.

For this exercise, responseMimeType plus your clear prompt is enough to explore the concept.

Task 4 — Convert generated JSON into your DTO

After extracting Gemini’s generated text, it should contain JSON.

Convert it:

QuizQuestion quizQuestion =
    mapper.readValue(
        generatedText,
        QuizQuestion.class
    );

You now have two JSON conversions:

HTTP response JSON
       |
       | ObjectMapper
       v
GeminiResponse DTO
       |
       | extract generated text
       v
Generated JSON
       |
       | ObjectMapper
       v
QuizQuestion DTO

This distinction is important.

The API response is JSON.

Inside that response, the LLM has generated another JSON document for your application.

Task 5 — Display the question

Print something similar to:

What does HTTP status 404 mean?

1. Unauthorized
2. Not Found
3. Internal Server Error
4. Created

Part 5 — Validate the generated data (50–55 min)

LLM-generated data should not automatically be trusted.

Validate your QuizQuestion.

Check that:

  • question is not null,
  • question is not blank,
  • answers is not null,
  • there are exactly four answers,
  • none of the answers are blank,
  • correctAnswer is between 0 and 3.

For example:

if (quizQuestion.answers() == null ||
    quizQuestion.answers().size() != 4) {

    throw new IllegalArgumentException(
        "Gemini returned an invalid quiz question"
    );
}

Add the remaining validation yourself.

Discussion question

Why should output from an LLM be treated similarly to input received from a user?


Part 6 — Final challenge: Make it interactive (55–60 min)

Turn the program into a tiny quiz application.

The application should:

  1. Ask the user for a topic.
  2. Insert the topic into the prompt.
  3. Send the prompt to Gemini.
  4. Convert the API response to GeminiResponse.
  5. Extract the generated JSON.
  6. Convert the JSON to QuizQuestion.
  7. Validate the question.
  8. Display it.
  9. Ask the user for an answer.
  10. Tell the user whether the answer was correct.

Example:

What topic would you like a question about?

> REST APIs

Generating question...

What does HTTP status 404 mean?

1. Unauthorized
2. Not Found
3. Internal Server Error
4. Created

Your answer:
> 2

Correct!

Your final architecture is:

             User
               |
               | topic
               v
        Java application
               |
               | prompt + JSON
               v
          Gemini API
               |
               | API response JSON
               v
        GeminiResponse
               |
               | generated JSON
               v
         QuizQuestion
               |
               | validation
               v
        Java application
               |
               v
             User

If you finish early

Generate an entire quiz.

Create:

public record Quiz(
    String topic,
    List<QuizQuestion> questions
) {
}

Ask the user for:

Topic:
> Java

Difficulty:
> medium

Number of questions:
> 5

Ask Gemini to generate the quiz as structured JSON.

Then run all questions and calculate the final score.

Example:

Java Quiz

Question 1/5
...

Question 2/5
...

Final score: 4/5

Discussion after the exercise

Be prepared to discuss:

  1. How is calling Gemini similar to calling a normal REST API?
  2. How is it different?
  3. Why does the wording of a prompt matter?
  4. Why is structured output useful?
  5. Why do we still need DTOs when using an LLM?
  6. Why should generated data be validated?
  7. What could happen if an application blindly trusts LLM output?
  8. Why should an API key never be committed to Git?
  9. What should the program do if Gemini returns HTTP 429, 500, or another error?
  10. When would a traditional API or ordinary Java code be a better solution than an LLM?

Key takeaway

You already knew this architecture:

REST API
   |
   v
 JSON
   |
   v
ObjectMapper
   |
   v
  DTO

Using an LLM extends the idea:

User input
    |
    v
  Prompt
    |
    v
 LLM API
    |
    v
API response JSON
    |
    v
Generated data
    |
    v
Validation
    |
    v
Java DTO
    |
    v
Application logic

The HTTP and JSON technologies are familiar.

The important new idea is that an LLM generates its response. Generated output can vary and can be incorrect, so application code must treat it as untrusted data and validate it before use.