This tutorial builds a production-shaped spring boot chatbot rest endpoint n4n that proxies to an OpenAI-compatible inference gateway. You will stand up a Spring Boot 3 service that accepts a chat message over HTTP and returns a model-generated reply, using a single external base URL and an API key.
Prerequisites
- Java 17+ and Maven 3.9+
- A free API key from an OpenAI-compatible gateway (we use the n4n.ai endpoint)
- Familiarity with Spring Boot dependency injection and
@RestController curlor any HTTP client for local testing
Set the key as an environment variable so it never lands in source control:
export N4N_API_KEY="sk-xxxxxxxxxxxxxxxx"
Project setup
Generate a minimal Spring Boot app. If you use the Spring Initializr, pick Spring Web and Java 17. The pom.xml only needs the web starter:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
Spring Boot 3.2+ ships RestClient, a synchronous HTTP client that removes most boilerplate from calling external JSON APIs. We will use it instead of RestTemplate.
Configuration
Externalize the gateway connection. Put this in src/main/resources/application.yml:
n4n:
base-url: https://api.n4n.ai/v1
api-key: ${N4N_API_KEY}
model: openai/gpt-4o-mini
server:
port: 8080
The base-url points at the OpenAI-compatible chat completions path prefix. The model field accepts any model identifier the gateway supports.
Calling the gateway
The chat completions API expects a JSON body with a messages array and a model string. We map it to Java records to keep the code strict and readable.
Request/response shapes
public record Message(String role, String content) {}
public record ChatRequest(List<Message> messages, String model) {}
public record Choice(Message message) {}
public record ChatResponse(List<Choice> choices) {}
The gateway returns choices[0].message.content as the assistant reply. Errors come back as standard HTTP status codes with a JSON error object; RestClient throws HttpClientErrorException on 4xx and HttpServerErrorException on 5xx.
Service layer
The ChatService builds the request, calls the endpoint, and extracts the reply. It also injects a system prompt to keep answers concise.
@Service
public class ChatService {
private final RestClient restClient;
private final String model;
public ChatService(@Value("${n4n.base-url}") String baseUrl,
@Value("${n4n.api-key}") String apiKey,
@Value("${n4n.model}") String model) {
this.model = model;
this.restClient = RestClient.builder()
.baseUrl(baseUrl)
.defaultHeader("Authorization", "Bearer " + apiKey)
.defaultHeader("Content-Type", "application/json")
.build();
}
public String ask(String userMessage) {
var messages = List.of(
new Message("system", "You are a terse assistant. Answer in one sentence."),
new Message("user", userMessage)
);
var request = new ChatRequest(messages, model);
ChatResponse response = restClient.post()
.uri("/chat/completions")
.body(request)
.retrieve()
.body(ChatResponse.class);
if (response == null || response.choices().isEmpty()) {
throw new IllegalStateException("Empty response from model gateway");
}
return response.choices().get(0).message().content();
}
}
This spring boot chatbot rest endpoint n4n delegates all model selection and inference to the gateway, so the local service stays thin.
REST controller
Expose a single POST endpoint. We use simple records for input and output to avoid leaking internal types.
public record ChatInput(String message) {}
public record ChatOutput(String reply) {}
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatService chatService;
public ChatController(ChatService chatService) {
this.chatService = chatService;
}
@PostMapping
public ChatOutput chat(@RequestBody ChatInput input) {
if (input.message() == null || input.message().isBlank()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "message required");
}
return new ChatOutput(chatService.ask(input.message()));
}
}
Running and testing
Start the app:
mvn spring-boot:run
In another shell, send a request:
curl -X POST http://localhost:8080/api/chat \
-H 'Content-Type: application/json' \
-d '{"message":"What is the capital of France?"}'
Expected output
{"reply":"The capital of France is Paris."}
If the gateway is unreachable, you will get a Spring Boot 500 with the underlying RestClient exception. For local development, that is enough signal. In production, wrap ask() with a @Retryable or a circuit breaker.
Resilience and routing
n4n.ai exposes a single OpenAI-compatible endpoint covering 240+ models and automatically fails over when a provider is rate-limited or degraded. Our spring boot chatbot rest endpoint n4n therefore avoids writing custom provider-pinning or retry logic for upstream outages. The gateway also honors client routing directives and forwards cache-control hints, so you can pass x-n4n-route headers if you later need to force a specific provider.
If you want per-token cost visibility, the gateway returns usage metadata in the usage field of the response. Extend ChatResponse to capture it:
public record Usage(int prompt_tokens, int completion_tokens, int total_tokens) {}
public record ChatResponse(List<Choice> choices, Usage usage) {}
Log usage for every call to feed internal metering.
Extensions
The current implementation is stateless. To support multi-turn conversation, store the messages list keyed by a session ID in a ConcurrentHashMap or Redis, and append the new user message before each call.
Streaming is the next lever. Swap RestClient for WebClient and consume the gateway’s text/event-stream response to push tokens to the browser over Server-Sent Events. The request body stays identical; only the stream: true flag and the parsing change.
Finally, add input validation and a rate limit on /api/chat using Spring Security or a reverse proxy. The model gateway already meters tokens; your endpoint should meter requests.
The spring boot chatbot rest endpoint n4n we built is deliberately minimal, but every piece—config, records, service, controller—maps directly to a production component you can harden.