n4nAI

Building a chatbot REST endpoint with Spring Boot and n4n

A practical step-by-step tutorial for building a Spring Boot chatbot REST endpoint with n4n's OpenAI-compatible LLM gateway, including config, Java code, and curl tests.

n4n Team3 min read559 words

Audio narration

Coming soon — every post will get a voice note here.

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
  • curl or 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.

Tagsspring-bootn4nchatbotrest-api

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All spring boot ai integration posts →