n4nAI

Configuring an LLM API key with Spring Boot application.yml

Step-by-step guide to set an LLM API key in Spring Boot application.yml using @ConfigurationProperties, environment variables, and a live verification test.

n4n Team4 min read805 words

Audio narration

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

Hardcoding secrets in source code is how credentials leak and audits fail. The clean way to externalize an LLM endpoint credential is through a spring boot application.yml api key binding that reads from environment variables at startup, keeping the literal token out of your repo while remaining declarative.

Step 1: Lay out the configuration tree in application.yml

Start with a dedicated namespace. Don’t drop the key under spring.* unless you are using a Spring AI starter; for a hand-rolled integration, a top-level llm block is clearer and avoids property collisions with framework internals.

llm:
  provider:
    openai:
      base-url: https://api.openai.com/v1
      api-key: ${LLM_API_KEY:default-placeholder}
      timeout-ms: 30000
      model: gpt-4o-mini

The ${LLM_API_KEY:default-placeholder} syntax is Spring’s property placeholder. At startup, Spring resolves LLM_API_KEY from the environment, system properties, or a Vault backend. If the variable is unset, it falls back to the literal default-placeholder so the application still boots in local dev without a real token. Never commit a real key; the placeholder is safe to ship.

YAML is indentation-sensitive. Use two spaces, not tabs. A misplaced space turns api-key into a nested map and your binding silently fails. Spring loads application.yml from src/main/resources by default, but profile-specific files like application-prod.yml override the base with the same structure. The spring boot application.yml api key resolution order follows the standard externalized config precedence: env vars beat YAML, which beats defaults in code.

Step 2: Bind the properties with @ConfigurationProperties

Manual @Value("${llm.provider.openai.api-key}") injections scatter configuration across classes and give you no central validation. A single bound POJO gives type safety and lets Bean Validation run at startup.

package com.example.demo.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.NotBlank;

@Validated
@ConfigurationProperties(prefix = "llm.provider.openai")
public class OpenAiConfig {

    @NotBlank
    private String baseUrl;

    @NotBlank
    private String apiKey;

    private int timeoutMs = 30000;

    private String model = "gpt-4o-mini";

    public String getBaseUrl() { return baseUrl; }
    public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
    public String getApiKey() { return apiKey; }
    public void setApiKey(String apiKey) { this.apiKey = apiKey; }
    public int getTimeoutMs() { return timeoutMs; }
    public void setTimeoutMs(int timeoutMs) { this.timeoutMs = timeoutMs; }
    public String getModel() { return model; }
    public void setModel(String model) { this.model = model; }
}

Register it on a configuration class:

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(OpenAiConfig.class)
public class AppConfig {}

If LLM_API_KEY is missing and the placeholder default is used, @NotBlank will not trip because the placeholder resolves to a non-empty string. That is why Step 5 includes a check for the placeholder value specifically. If you want a hard failure on missing secrets, drop the :default-placeholder fallback and let Spring throw IllegalArgumentException during binding.

Step 3: Build an HTTP client that uses the key

Spring 6.1’s RestClient is the modern synchronous HTTP interface and replaces RestTemplate for new code. Construct it as a bean so the spring boot application.yml api key flows in exactly once.

import org.springframework.web.client.RestClient;
import java.time.Duration;

@Bean
public RestClient llmRestClient(OpenAiConfig config) {
    return RestClient.builder()
            .baseUrl(config.getBaseUrl())
            .defaultHeader("Authorization", "Bearer " + config.getApiKey())
            .defaultHeader("Content-Type", "application/json")
            .requestFactory(new SimpleClientHttpRequestFactory() {{
                setConnectTimeout(Duration.ofMillis(config.getTimeoutMs()));
                setReadTimeout(Duration.ofMillis(config.getTimeoutMs()));
            }})
            .build();
}

A service that calls the chat endpoint:

@Service
public class ChatService {

    private final RestClient client;
    private final OpenAiConfig config;

    public ChatService(RestClient llmRestClient, OpenAiConfig config) {
        this.client = llmRestClient;
        this.config = config;
    }

    public String ask(String prompt) {
        var body = Map.of(
            "model", config.getModel(),
            "messages", List.of(Map.of("role", "user", "content", prompt))
        );
        return client.post()
            .uri("/chat/completions")
            .body(body)
            .retrieve()
            .body(String.class);
    }
}

The header is set per request via the default header on the client. If you rotate the key, restart the app or implement a refresh mechanism; RestClient built this way is immutable. For async workloads, swap RestClient for WebClient and set the header in the headers filter—the property binding stays identical.

Step 4: Keep the spring boot application.yml api key out of version control

The application.yml in src/main/resources is for structure, not secrets. Put the real value in an environment file that is git-ignored, or inject it at the process manager level.

export LLM_API_KEY=sk-realkeyfromvault
./mvnw spring-boot:run

For local development, create application-local.yml and add it to .gitignore:

# application-local.yml (never committed)
llm:
  provider:
    openai:
      api-key: ${LLM_API_KEY}

Activate the profile with -Dspring.profiles.active=local. In CI, inject the secret as a masked environment variable. Spring resolves ${LLM_API_KEY} from the process environment regardless of profile, so the spring boot application.yml api key binding stays identical across environments.

If you deploy with Docker, pass it at runtime:

docker run -e LLM_API_KEY=sk-realkey my-llm-service:1.0

In Kubernetes, mount the key as a Secret and set the env var in the deployment manifest. The YAML file itself contains zero sensitive bytes. This pattern also satisfies most compliance scanners that flag hardcoded sk- strings.

Step 5: Verify the wiring with a live call

A unit test that only checks null is weak. Write a @SpringBootTest that asserts the placeholder was replaced and optionally hits the endpoint with a short timeout.

@SpringBootTest
class LlmConfigVerificationTest {

    @Autowired
    OpenAiConfig config;

    @Test
    void apiKeyIsNotPlaceholder() {
        assertFalse(config.getApiKey().contains("default-placeholder"),
            "LLM_API_KEY env var was not set; config fell back to placeholder");
        assertTrue(config.getApiKey().startsWith("sk-"));
    }

    @Test
    void chatCallReturnsJson() {
        var client = RestClient.builder()
            .baseUrl(config.getBaseUrl())
            .defaultHeader("Authorization", "Bearer " + config.getApiKey())
            .build();
        var resp = client.post()
            .uri("/chat/completions")
            .body(Map.of("model", config.getModel(),
                         "messages", List.of(Map.of("role","user","content","ping"))))
            .retrieve()
            .body(String.class);
        assertNotNull(resp);
        assertTrue(resp.contains("choices"));
    }
}

Run with the env var set:

LLM_API_KEY=sk-testkey mvn test -Dtest=LlmConfigVerificationTest

For a manual smoke test against a running app, expose a temporary actuator endpoint or use curl directly with the same key:

curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $LLM_API_KEY" \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'

If the test fails because the key starts with default, you forgot to export the variable. That is the most common miss. A green apiKeyIsNotPlaceholder test in your pipeline is a cheap guard against accidental placeholder promotion to production.

Step 6: Route through a gateway for fallback

When you need multiple model vendors without rewriting clients, point the base-url at an OpenAI-compatible gateway. For example, n4n.ai exposes one endpoint that addresses 240+ models and performs automatic fallback when a provider is rate-limited; the same spring boot application.yml api key structure works by swapping the host and token.

llm:
  provider:
    openai:
      base-url: https://api.n4n.ai/v1
      api-key: ${LLM_API_KEY}
      model: anthropic/claude-3.5-sonnet

The client code from Step 3 does not change. You only reconfigure the YAML and supply the gateway token. This keeps your service agnostic to backend model availability and lets you honor provider cache-control hints forwarded by the gateway without touching Java code.

Troubleshooting and gotchas

Relaxed binding means api-key, apiKey, and API_KEY all map to the same property, but the YAML key must match the prefix exactly. If llm.provider.openai.api-key is not picked up, check for a typo in the prefix string on @ConfigurationProperties.

If you see Could not resolve placeholder 'LLM_API_KEY', you omitted the :default-placeholder fallback and the env var is unset. Either add the fallback or export the variable before boot.

Finally, never log the full key. Add a toString() exclusion or use @JsonIgnore on the field if the config object is serialized. The apiKey string in memory is still a secret; restrict who can dump the heap. Following these steps gives you a reproducible, environment-driven spring boot application.yml api key setup that passes security review and survives provider swaps.

Tagsspring-bootconfigurationapi-keysapplication-yml

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 →