Claude API Java Spring Boot Setup Guide
Setting up the Claude API in a Spring Boot project means configuring an HTTP client bean, modeling the request/response JSON as Java records or POJOs, and wiring your API key through Spring's configuration properties. This guide walks through a complete setup using WebClient (the recommended modern client), with a RestTemplate alternative for older codebases that haven't migrated yet.
The core challenge isn't Spring-specific — it's that you need an HTTP client, JSON serialization that matches Anthropic's message schema, and proper handling of streaming responses if you want them. Below is a working setup you can drop into any Spring Boot 3.x project.
Project Dependencies
You don't need Anthropic's SDK — a plain HTTP client is enough since the API is just REST + JSON. Add these to your pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
spring-boot-starter-webflux gives you WebClient, which handles streaming far better than RestTemplate.
Configuration Properties
Keep your API key out of source control. In application.yml:
claude:
api-key: ${CLAUDE_API_KEY}
base-url: https://api.anthropic.com/v1
model: claude-3-5-sonnet-20241022
Bind it with a configuration class:
@ConfigurationProperties(prefix = "claude")
public record ClaudeProperties(String apiKey, String baseUrl, String model) {}
Enable it in your main application class or a @Configuration class with @EnableConfigurationProperties(ClaudeProperties.class).
WebClient Bean
@Configuration
public class ClaudeClientConfig {
@Bean
public WebClient claudeWebClient(ClaudeProperties props) {
return WebClient.builder()
.baseUrl(props.baseUrl())
.defaultHeader("x-api-key", props.apiKey())
.defaultHeader("anthropic-version", "2023-06-01")
.defaultHeader("content-type", "application/json")
.build();
}
}
Request and Response Models
Model the message schema with Java records — immutable, concise, and perfect for DTOs:
public record ClaudeMessage(String role, String content) {}
public record ClaudeRequest(
String model,
int max_tokens,
List<ClaudeMessage> messages
) {}
public record ClaudeResponse(
String id,
String role,
List<ContentBlock> content,
Usage usage
) {
public record ContentBlock(String type, String text) {}
public record Usage(int input_tokens, int output_tokens) {}
}
The Service Class
@Service
public class ClaudeService {
private final WebClient webClient;
private final ClaudeProperties props;
public ClaudeService(WebClient claudeWebClient, ClaudeProperties props) {
this.webClient = claudeWebClient;
this.props = props;
}
public ClaudeResponse sendMessage(String prompt) {
ClaudeRequest request = new ClaudeRequest(
props.model(),
1024,
List.of(new ClaudeMessage("user", prompt))
);
return webClient.post()
.uri("/messages")
.bodyValue(request)
.retrieve()
.bodyToMono(ClaudeResponse.class)
.block();
}
}
Expose it through a controller:
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ClaudeService claudeService;
public ChatController(ClaudeService claudeService) {
this.claudeService = claudeService;
}
@PostMapping
public ClaudeResponse chat(@RequestBody Map<String, String> body) {
return claudeService.sendMessage(body.get("prompt"));
}
}
This gives you a working /api/chat endpoint that proxies prompts to Claude and returns the parsed response.
Handling Streaming in Spring
If you want token-by-token streaming, use WebClient's reactive bodyToFlux against a server-sent events stream instead of bodyToMono, and return a Flux<String> from a @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE) endpoint. This requires parsing SSE frames yourself, tracking content_block_delta events, and reassembling partial JSON — doable, but it's a meaningful chunk of extra code that has nothing to do with your actual business logic.
Error Handling and Retries
Claude's API returns standard HTTP error codes — 401 for bad keys, 429 for rate limits, 529 for overload. Wrap calls with onStatus in WebClient:
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, resp ->
resp.bodyToMono(String.class).map(body ->
new ResponseStatusException(resp.statusCode(), body)))
.onStatus(HttpStatusCode::is5xxServerError, resp ->
Mono.error(new ServiceUnavailableException("Claude API unavailable")))
Pair this with Spring Retry (@Retryable) on 429/529 responses using exponential backoff, since Anthropic's rate limits are per-organization and bursts are common in production traffic.
Simplifying the Setup
All of this — client configuration, DTO modeling, streaming parsers, retry logic, key rotation — is boilerplate every Java team writing against Claude ends up rebuilding. If you'd rather skip straight to calling a stable REST endpoint, SubToAPI wraps your Claude access behind a single HTTPS API with application keys (sub_live_...), so your Spring service just points at https://api.subtoapi.app/v1/messages instead of managing Anthropic auth headers directly:
WebClient.builder()
.baseUrl("https://api.subtoapi.app/v1")
.defaultHeader("Authorization", "Bearer " + subToApiKey)
.build();
This gives you streaming, tool use, and usage metadata without hand-rolling SSE parsing in your controller layer. Check /docs/quickstart for the request format and /docs/streaming for the SSE details, or compare /pricing if you're managing multiple developer seats under one Claude subscription.
questions
Do I need Anthropic's official SDK to use Claude in a Spring Boot app? No. Anthropic doesn't publish an official Java SDK, so most Spring Boot integrations use WebClient or RestTemplate directly against the REST API, as shown above.
Should I use RestTemplate or WebClient for Claude API calls? WebClient is strongly preferred — it's non-blocking, integrates cleanly with Spring's reactive stack, and is the only practical option if you plan to support streaming responses later.
How do I keep my Claude API key secure in a Spring Boot deployment? Load it from environment variables or a secrets manager (AWS Secrets Manager, Vault) via @ConfigurationProperties, never hardcode it in application.yml, and exclude .env files from version control.