This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
See the root CLAUDE.md for cross-cutting patterns.
# From repo root
./gradlew :server-api:build # Build
./gradlew :server-api:test # Run all testsserver-api is a reusable Spring Boot server framework library on Spring Boot 4.0 / Spring Framework 7.0 / Spring Security 7.0. It provides Spring 7's native API versioning, Spring Security-backed API key authentication, rate limiting (Bucket4j), error handling, and server configuration as a java-library that other Spring Boot applications consume. Follows the same pattern as discord-api (framework) vs simplified-bot (implementation).
Exports: Spring Boot starters (web, actuator, security), bucket4j-core, gson, and the simplified-dev client and gson-extras libraries via api() dependencies. Consumers get all of these transitively, including @PreAuthorize, Authentication, the @RequestMapping(version=...) attribute, and the Bucket4j Bucket API.
config/ - Server-wide configuration:
ServerConfig- Immutable configuration class following theClassBuilderpattern. InnerBuilderwith@BuildFlagvalidation. Static factories:builder()for full control,optimized()for a production-tuned preset.toProperties()converts fields to aConcurrentMap<String, Object>forSpringApplication.setDefaultProperties(). IncludesspringdocEnabledtoggle controlling SpringDoc/Scalar properties.ServerWebConfig- Framework-levelWebMvcConfigurerproviding theGsonHttpMessageConverter(placed ahead of Jackson), the no-opErrorControllerbean that displaces Spring Boot'sBasicErrorController, and the sharedErrorResponseWriterbean. Uses a consumer-providedGson@Beanif available, otherwise falls back to a defaultGsoncreated fromGsonSettings.defaults(). Security response headers are set by Spring Security'sHeadersConfigurerinApiKeySecurityConfigrather than here.ApiVersionWebConfig-WebMvcConfigurerwiring Spring 7's path-segment API versioning via two predicates:usePathSegment(0, requestPathPredicate)gates version extraction to URLs matching/v<digits>/...(so non-matching paths bypass extraction);addPathPrefix("/{version}", classPredicate)injects the/{version}prefix on any controller whose methods declareversion=(so handlers don't repeat/v1/in theirpath=). The defaultSemanticApiVersionParserstrips thevprefix and parses semver, so@GetMapping(path = "/hello", version = "1")is reachable at/v1/hello. Constraint:addPathPrefixis class-level, so don't mix versioned and unversioned methods on the same controller.
error/ - Global error handling and HTML error page rendering:
ErrorController- Global@RestControllerAdviceextendingResponseEntityExceptionHandler. Delegates content-negotiated rendering toErrorResponseWriter. Includes explicit handlers forAccessDeniedExceptionandAuthenticationExceptionthrown by@PreAuthorizefrom inside controllers (these unwind to dispatcher servlet exception handling before Spring Security'sExceptionTranslationFiltersees them, so we route them to the same writer used by the entry point and access-denied handler). Mirrors the filter's "anonymous becomes 401" behavior. Also handlesMissingApiVersionExceptionandInvalidApiVersionExceptionthrown by Spring 7's versioning machinery, mapping them to 400.ErrorResponseWriter- Shared utility for content-negotiated error responses (HTML vs JSON). Used byErrorController(returnsResponseEntity) and by Spring Security'sAuthenticationEntryPoint/AccessDeniedHandler(writes directly to the response). Holds theGsoninstance for JSON serialization.ErrorPageRenderer- Non-instantiable utility class rendering Cloudflare-style HTML error pages. ContainsPlaceholderenum for named{{TOKEN}}substitution with XSS escaping, andErrorSourceenum (CLIENT,SERVER,API).
security/ - Spring Security-backed API key authentication and authorization:
ApiKey- Authenticated principal carrying the key string, assignedApiKeyRoles, and rate-limit configuration (maxRequests,windowInSeconds).getAuthorities()derivesSimpleGrantedAuthority("ROLE_" + name)from the role set.ApiKeyRole- Enum of hierarchical access roles. Declaration order defines the hierarchy (earlier constants inherit later constants' authorities).getAuthority()returns"ROLE_" + name().ApiKeyStore- SPI interface consumers implement to supplyApiKeyinstances. Single methodfindByKey(String). Implementations are free to return fresh instances on each lookup since rate-limit state is held externally inRateLimitFilter.InMemoryApiKeyStore- Public referenceApiKeyStoreimplementation backed by a concurrent map. Suitable for tests, local development, and stopgap production wiring before a persistent store is available.ApiKeyAuthenticationToken-AbstractAuthenticationTokencarrying anApiKeyas principal. Two-stage construction:unauthenticated(String)+authenticated(ApiKey, Collection<GrantedAuthority>).ApiKeyAuthenticationFilter-OncePerRequestFilterreading theX-API-Keyheader and delegating to theAuthenticationManager. On failure invokes the entry point directly so the response is rendered immediately. Added beforeUsernamePasswordAuthenticationFilterin the chain.ApiKeyAuthenticationProvider-AuthenticationProviderresolving an unauthenticated token viaApiKeyStore.findByKey(). ThrowsBadCredentialsExceptionfor unknown keys.ApiKeyAuthenticationEntryPoint-AuthenticationEntryPointrendering content-negotiated 401 responses viaErrorResponseWriter.ApiKeyAccessDeniedHandler-AccessDeniedHandlerrendering content-negotiated 403 responses viaErrorResponseWriter.ApiKeyRateLimitFilter-OncePerRequestFilterconsulting Bucket4jBuckets keyed byApiKey.getKeyValue(). Capacity and refill come fromApiKey.getMaxRequests()andApiKey.getWindowInSeconds(). Anonymous requests pass through. Inserted afterApiKeyAuthenticationFilterin the chain. On overflow it writes a 429 response directly viaErrorResponseWriterrather than throwing - filter exceptions don't reach@RestControllerAdvice, so direct rendering keeps the response content-negotiated.ApiKeySecurityConfig-@EnableWebSecurity+@EnableMethodSecurity+@ConditionalOnProperty(name = "api.key.authentication.enabled", havingValue = "true"). Declares theSecurityFilterChain(stateless, CSRF-disabled,permitAllat the chain level so@PreAuthorizeis the gating mechanism), theRoleHierarchy(built fromApiKeyRoledeclaration order), theMethodSecurityExpressionHandlerwired with the hierarchy, theAuthenticationManager, theApiKeyRateLimitFilterbean, and the security headers (X-Content-Type-Options, HSTS,X-Frame-Options: DENY,Referrer-Policy: no-referrer, X-XSS-Protection). Intentionally declares no defaultApiKeyStorebean - consumers must supply one, otherwise startup fails fast withNoSuchBeanDefinitionException. A silent empty fallback would 401 every request and be extremely confusing to debug.PermitAllSecurityConfig- Fallback@EnableWebSecurityconfig active whenapi.key.authentication.enabledis missing orfalse. Permits all requests so Spring Boot's defaultSecurityAutoConfigurationdoes not install a basic-auth chain with a generated password (which is rarely desired).security/openapi/- SpringDoc customizers (ApiKeyOpenApiConfig,ApiKeySecurityCustomizer,ApiKeyOperationCustomizer). The operation customizer scans@PreAuthorizeannotations and infers the qualifyingApiKeyRoleset fromhasRole/hasAnyRole/hasAuthorityexpressions for documentation purposes.
API versioning is provided by Spring Framework 7 directly. There is no version/ package - declare the version on each handler with @RequestMapping(version = "1") (or @GetMapping(path = ..., version = "1")) and the framework routes accordingly. Configuration lives in config/ApiVersionWebConfig.
src/main/resources/error/ - HTML error page resources:
error-page.css- Minified CSS from donlon/cloudflare-error-page (MIT license).error-page.html- HTML template with named{{PLACEHOLDER}}tokens.
TestServer - Minimal @SpringBootApplication for testing the framework. Boots a lightweight server with API versioning, API key authentication, error handling, and the test controllers. Uses ServerConfig.builder() defaults with SpringDoc disabled. Run main() from the IDE to start on port 8080.
controller/ - Test controllers exercising framework features:
TestApiKeyController- Endpoints under/api/demonstrating@PreAuthorizewith role requirements (ADMIN,DEVELOPER,USER) resolved through theApiKeyRolehierarchy.TestVersionController- Endpoints demonstrating Spring 7's@RequestMapping(version=...)with multiple versions (/v1/hello,/v2/hello,/v3/hello,/v1/data,/v2/data). Handlers declareversion="N"andpath="/hello"only; the/v{version}prefix is injected automatically becauseApiVersionWebConfig's class-level predicate detects that the controller has version-bearing methods.TestUnversionedController- The/defaultendpoint, on its own controller because the path-prefix mechanism is class-level (a single mixed controller would prefix the unversioned method too).
Consumers must scan the dev.sbs.serverapi package for Spring to pick up configuration beans:
@SpringBootApplication(scanBasePackages = { "com.example.myapp", "dev.sbs.serverapi" })
public class MyApplication { }To customize the Gson instance used by the framework's message converters, define a @Bean:
@Bean
public Gson gson() {
return myCustomGson;
}When api.key.authentication.enabled=true (the default), consumers must provide an ApiKeyStore bean. For quick bring-up, seed an InMemoryApiKeyStore:
@Bean
public ApiKeyStore apiKeyStore() {
return new InMemoryApiKeyStore()
.put(new ApiKey("my-key", Concurrent.newSet(ApiKeyRole.USER), 100, 60));
}Protect controllers with @PreAuthorize:
@RestController
@PreAuthorize("isAuthenticated()") // class-level: any valid key
@RequestMapping("/api")
public class MyController {
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')") // method-level: ADMIN-or-higher
public String adminOnly() { ... }
}api.key.authentication.enabled- Toggles API key security. Whentrue,ApiKeySecurityConfigactivates. Whenfalse(or missing),PermitAllSecurityConfigactivates with a permit-all chain. DefaulttrueinServerConfig.springdocEnabled- Toggle inServerConfigcontrolling SpringDoc property output.ServerConfig.builder()/ServerConfig.optimized()for programmatic server tuning.