Skip to main content

Command Palette

Search for a command to run...

Spring AI ChatClient API: The Fluent Heart of AI Integration

Updated
5 min read

Spring AI ChatClient API: The Fluent Heart of AI Integration

Introduction

If you've ever tried integrating AI models into a Java application, you know the pain. HTTP clients, API keys scattered everywhere, vendor-specific SDKs that never quite fit. What should take minutes takes days.

Spring AI's ChatClient API changes everything. It's a fluent, intuitive interface that makes calling AI models feel as natural as calling any other Spring service. In this post, we'll dive deep into the ChatClient API — the core building block that everything else in Spring AI builds upon.


What is ChatClient?

At its core, ChatClient is Spring AI's abstraction over AI model communication. Think of it as the RestTemplate or WebClient for AI — but designed specifically for the conversational nature of LLMs.

The API follows three principles:

  1. Fluent: Method chaining that reads like English
  2. Flexible: Sync or streaming, your choice
  3. Portable: Same code works across OpenAI, Anthropic, Groq, and more

Creating a ChatClient

The Builder Pattern

@Bean
ChatClient chatClient(ChatModel chatModel) {
    return ChatClient.builder(chatModel)
        .defaultSystem("You are a helpful Spring Boot expert")
        .build();
}

Notice something beautiful here? No API keys. No HTTP configuration. Just the ChatModel — which Spring Boot has already autoconfigured from your application.properties.

Autoconfiguration Magic

Spring AI's autoconfiguration creates a prototype ChatClient.Builder bean. Inject it and build:

@RestController
class MyController {
    private final ChatClient chatClient;

    public MyController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }
}

The Prompt → Call → Content Chain

Here's where ChatClient shines. The fluent API follows a natural left-to-right flow:

Basic Synchronous Call

String response = chatClient.prompt()
    .user("Explain Spring Boot in one sentence")
    .call()
    .content();

Breaking it down:

  • prompt() — Starts the conversation. Add system messages, user messages, conversation history
  • call() — Executes synchronously, waits for full response
  • content() — Extracts just the text content

Getting Full Response Metadata

Need more than just text? Use .chatResponse() instead:

ChatResponse response = chatClient.prompt()
    .user("Explain Spring Boot")
    .call()
    .chatResponse();

// Access metadata
Generation generation = response.getResult();
String finishReason = generation.getMetadata().getFinishReason();
int tokenCount = generation.getMetadata().getUsage().getTotalTokens();

Sync vs Streaming

Synchronous Calls

Best for: Quick operations where you need the complete answer before continuing.

String answer = chatClient.prompt()
    .user("What is dependency injection?")
    .call()
    .content();

Streaming Calls

Best for: Chat interfaces, long-form content, real-time updates.

Flux<String> tokens = chatClient.prompt()
    .user("Write a detailed Spring Boot tutorial")
    .stream()
    .content();

// Subscribe and display tokens as they arrive
tokens.subscribe(System.out::println);

The beauty? Just swap call() for stream() and you get a reactive Flux of tokens. Spring AI handles all the Server-Sent Events (SSE) complexity.


Entity Mapping: AI Meets POJOs

This is where ChatClient becomes production-ready. Map AI responses directly to Java objects:

record Movie(String title, String director, int year) {}

Movie recommendation = chatClient.prompt()
    .user("Recommend a classic programming movie")
    .call()
    .entity(Movie.class);

System.out.println(recommendation.title()); // "The Social Network"

Behind the scenes, Spring AI:

  1. Instructs the model to return structured JSON
  2. Parses the response
  3. Maps to your record/class
  4. Handles type conversion and validation

No more regex parsing. No more manual JSON mapping.


Multiple Model Configuration

Real applications often need multiple AI models. Spring AI makes this elegant with @Qualifier:

@Configuration
class ChatClientConfig {

    @Bean
    @Qualifier("openai")
    ChatClient openAiClient(ChatModel openAiModel) {
        return ChatClient.builder(openAiModel)
            .defaultSystem("You are an expert coder")
            .build();
    }

    @Bean
    @Qualifier("anthropic")
    ChatClient anthropicClient(ChatModel anthropicModel) {
        return ChatClient.builder(anthropicModel)
            .defaultSystem("You are a creative writer")
            .build();
    }
}

// Usage
@Service
class AIService {
    @Autowired
    @Qualifier("openai")
    private ChatClient openAiClient;

    @Autowired
    @Qualifier("anthropic")
    private ChatClient anthropicClient;
}

Production Tips

1. Default System Prompts

Set application-wide behavior in the builder:

return ChatClient.builder(chatModel)
    .defaultSystem("You are a helpful assistant. Be concise.")
    .defaultOptions(ChatOptions.builder()
        .temperature(0.7)
        .build())
    .build();

2. Request/Response Logging

For debugging, intercept the ChatClient calls with an Advisor (covered in Lecture 4):

chatClient.prompt()
    .advisors(new LoggingAdvisor())
    .user("Hello")
    .call();

3. Error Handling

Wrap calls in try-catch for model-specific exceptions:

 try {
    return chatClient.prompt()
        .user(userInput)
        .call()
        .content();
} catch (AiException e) {
    // Handle rate limits, token limits, etc.
    return "Service temporarily unavailable";
}

Complete REST Controller Example

Here's a production-ready controller with multiple model configs and streaming:

@RestController
@RequestMapping("/api/ai")
public class AIController {

    private final ChatClient defaultClient;
    private final ChatClient streamingClient;

    public AIController(
            @Qualifier("openai") ChatClient defaultClient,
            @Qualifier("groq") ChatClient streamingClient) {
        this.defaultClient = defaultClient;
        this.streamingClient = streamingClient;
    }

    // Sync endpoint for quick responses
    @GetMapping("/ask")
    public ResponseEntity<String> ask(@RequestParam String question) {
        String answer = defaultClient.prompt()
            .user(question)
            .call()
            .content();
        return ResponseEntity.ok(answer);
    }

    // Streaming endpoint for chat-like experience
    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> stream(@RequestParam String question) {
        return streamingClient.prompt()
            .user(question)
            .stream()
            .content();
    }

    // Entity mapping endpoint
    @PostMapping("/analyze")
    public ResponseEntity<Analysis> analyze(@RequestBody String text) {
        Analysis result = defaultClient.prompt()
            .user("Analyze this text: " + text)
            .call()
            .entity(Analysis.class);
        return ResponseEntity.ok(result);
    }

    public record Analysis(String sentiment, List<String> keyPoints) {}
}

What's Next?

Now that you understand the ChatClient API, the next lecture covers Working with Multiple AI Models — how to configure, compare, and combine different model providers in a single application.


Key Takeaways

  1. ChatClient.Builder gives you clean, configurable AI model access
  2. prompt() → call() → content() is your bread-and-butter chain
  3. Use streaming for real-time UX, sync for quick operations
  4. Entity mapping eliminates boilerplate JSON parsing
  5. @Qualifier pattern enables multiple model configurations

Series: Spring AI Complete Course Next: Lecture 3 — Working with Multiple AI Models Code samples: [GitHub Repository Link]

#springai #java #springboot #ai #chatclient #llm #tutorial