# Spring AI ChatClient API: The Fluent Heart of AI Integration

# 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

```java
@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:

```java
@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

```java
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:

```java
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.

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

### Streaming Calls

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

```java
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:

```java
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`:

```java
@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:

```java
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):

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

### 3. Error Handling

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

```java
 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:

```java
@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
