Course topics

By WebNest Studio

Spring Boot Tutorial

ChatClient and Prompt Templates

ChatClient is the API you will use for nearly every interaction with a language model in Spring AI. Its fluent style — prompt(), then system() and user(), then call() or stream() — makes the structure of each request obvious and keeps application code independent of the provider.

In this lesson you will learn how messages and roles work, how to configure defaults once, how to use prompt templates with parameters and external template files, how to stream responses token by token, how to read token usage, how to override model options per request, and how to send images to multimodal models.

Messages and Roles

A chat request is a list of messages, each with a role. The system message sets behaviour, tone and rules ("You are a support assistant for Webnest. Answer only questions about our courses."). User messages carry the end user's input. Assistant messages are previous model replies, used to continue a conversation. Tool messages carry results of tool calls. Models give system instructions high priority, so put your rules there, not in the user text.

Defaults on the Builder

ChatClient.Builder lets you set defaults that apply to every request: defaultSystem(...), defaultOptions(...), defaultAdvisors(...) and defaultTools(...). Build a ChatClient bean per "persona" or use case (support bot, SQL assistant, summariser) and inject the one you need. Per-request calls can still override or add to the defaults.

Prompt Templates

Spring AI uses {placeholder} syntax (rendered with the StringTemplate engine) for both system and user text. Pass values with .param(name, value). For long prompts, keep templates in files such as src/main/resources/prompts/summarise.st, inject them as a Resource, and pass the resource to system(...) or user(...). Keeping prompts outside Java code makes them easy to review and version.

call() vs stream()

call() waits for the complete answer. stream() returns a Reactor Flux that emits pieces of text as the model generates them, so users see the answer appear immediately — the same experience as ChatGPT. Spring MVC can return a Flux<String> directly as Server-Sent Events. After call() you can ask for content() (just text), chatResponse() (text plus metadata such as token usage and finish reason), or entity(...) (structured output, next lesson).

Model Options

Options control generation: model, temperature (0 = focused and repeatable, higher = more varied), maxTokens and provider-specific settings. Set defaults in properties, and override per request with the provider's options builder, for example OpenAiChatOptions.builder().model("gpt-5").temperature(0.0).build(). In Spring AI 2.0 options objects are immutable and created with builders.

Multimodal Input

Vision-capable models accept images alongside text. Add media to the user message with .user(u -> u.text(...).media(mimeType, resource)). This is useful for describing product photos, reading receipts, or extracting data from screenshots.

Examples

Several ChatClient beans with different defaults

Java
@Configuration
public class ChatClientConfig {

    @Bean
    ChatClient supportClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem("""
                You are the support assistant for Webnest Studio.
                Answer only questions about Webnest courses and accounts.
                If you do not know the answer, say so and suggest contacting support@webneststudio.co.in.
                """)
            .build();
    }

    @Bean
    ChatClient summaryClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem("Summarise the given text in at most {sentences} sentences for a {audience} audience.")
            .build();
    }
}
Output
(Inject by parameter name or with @Qualifier("supportClient") / @Qualifier("summaryClient").)

Templates with parameters and an external template file

Java
// src/main/resources/prompts/explain.st
// Explain the Java topic "{topic}" to a {level} developer.
// Use one short code example and finish with two practice questions.

@Service
public class TutorService {

    private final ChatClient summaryClient;
    private final ChatClient tutor;

    @Value("classpath:/prompts/explain.st")
    private Resource explainTemplate;

    public TutorService(ChatClient summaryClient, ChatClient.Builder builder) {
        this.summaryClient = summaryClient;
        this.tutor = builder.build();
    }

    public String summarise(String text) {
        return summaryClient.prompt()
            .system(s -> s.param("sentences", 3).param("audience", "beginner"))
            .user(text)
            .call()
            .content();
    }

    public String explain(String topic, String level) {
        return tutor.prompt()
            .user(u -> u.text(explainTemplate).param("topic", topic).param("level", level))
            .call()
            .content();
    }
}
Output
explain("records", "beginner")

A record is a compact class for holding immutable data...
    record Point(int x, int y) {}
Practice: 1) What methods does a record generate automatically? 2) Can a record extend another class?

Streaming a response to the browser with Server-Sent Events

Java
@RestController
public class StreamController {

    private final ChatClient chatClient;

    public StreamController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @GetMapping(value = "/ask/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> stream(@RequestParam String question) {
        return chatClient.prompt()
            .user(question)
            .stream()
            .content();
    }
}

// Browser:
// const es = new EventSource('/ask/stream?question=' + encodeURIComponent(q));
// es.onmessage = (e) => output.textContent += e.data;
Output
curl -N "http://localhost:8080/ask/stream?question=Explain+dependency+injection"
data:Dependency
data: injection
data: means
data: an
data: object
...

Reading metadata and overriding options per request

Java
ChatResponse response = chatClient.prompt()
    .user("Give me three names for a Java learning platform.")
    .options(OpenAiChatOptions.builder()
        .model("gpt-5")
        .temperature(0.9)
        .build())
    .call()
    .chatResponse();

Usage usage = response.getMetadata().getUsage();
System.out.println(response.getResult().getOutput().getText());
System.out.println("model=" + response.getMetadata().getModel()
    + " prompt=" + usage.getPromptTokens()
    + " completion=" + usage.getCompletionTokens()
    + " total=" + usage.getTotalTokens());
Output
1. CodeForge Academy
2. JavaNest
3. Bytecode Bootcamp
model=gpt-5 prompt=21 completion=18 total=39

Sending an image to a vision model

Java
@PostMapping(value = "/describe", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String describe(@RequestParam MultipartFile image) {
    return chatClient.prompt()
        .user(u -> u.text("Describe this product photo in one sentence for an online shop.")
                    .media(MimeTypeUtils.parseMimeType(image.getContentType()), image.getResource()))
        .call()
        .content();
}
Output
curl -F image=@backpack.png http://localhost:8080/describe
A navy-blue waterproof laptop backpack with padded straps and a front zip pocket.

Common Mistakes

  • Concatenating user input into the system prompt, which lets users rewrite your instructions. Keep rules in the system message and user text in user().
  • Creating a new ChatClient on every request with ChatClient.builder(...) instead of reusing a configured bean.
  • Using call() for long answers in a chat UI; users wait seconds with a blank screen when stream() would show progress.
  • Forgetting that literal curly braces in templates (for example JSON examples) are treated as placeholders and must be escaped.
  • Setting a high temperature for tasks that need consistent, factual answers such as classification or extraction.

Key Points to Remember

  • ChatClient: prompt() → system()/user() → call() or stream() → content(), chatResponse() or entity().
  • Configure defaultSystem, defaultOptions, defaultAdvisors and defaultTools once on the builder.
  • Templates use {placeholders} filled with .param(); keep long prompts in resource files.
  • stream() returns a Flux you can expose as Server-Sent Events for a responsive UI.
  • chatResponse() exposes token usage; per-request options override model and temperature.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.