BFF ships with io.github.rthadani.bff.Bff — a static Java facade that
handles Clojure loading and Jakarta servlet wiring for you. Extensions are
supplied through an immutable BffConfig at startup; there is no global
registry and no runtime registration.
pom.xml)<dependency>
<groupId>io.github.rthadani</groupId>
<artifactId>bff</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.clojure</groupId>
<artifactId>clojure</artifactId>
<version>1.12.0</version>
</dependency>
jakarta.servlet-api is declared as provided in the BFF POM — your Spring
Boot 3 starter already brings its own copy, so nothing more to add. Place your
spec file in src/main/resources/.
Build a BffConfig from your Spring beans, then hand it to
Bff.createServlet behind a ServletRegistrationBean. All extensions are
captured into the handler at this point — there is no register-then-create
dance.
import io.github.rthadani.bff.Bff;
import io.github.rthadani.bff.BffConfig;
import jakarta.servlet.http.HttpServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BffConfiguration {
@Bean
public BffConfig bffConfig(BffExtensions ext) {
return BffConfig.builder()
.enricher (ext.customerEnricher())
.validator ("check-order", ext.orderValidator())
.transformer("attach-warnings", ext.warningsTransformer())
.resolver ("user-profile", ext.userProfileResolver())
.retryHook ("cmap-token-refresh", ext.tokenRefreshHook())
.cache(ext.redisCacheStore())
.build();
}
@Bean
public ServletRegistrationBean<HttpServlet> bffServlet(BffConfig config) {
ServletRegistrationBean<HttpServlet> reg =
new ServletRegistrationBean<>(Bff.createServlet("bff-spec.yaml", config), "/graphql");
reg.setName("bff");
reg.setLoadOnStartup(1);
return reg;
}
}
For a controller-style mount instead of the servlet, use
Bff.createHandler(specPath, config) and call the returned IFn yourself.
Extensions can implement the raw Java interfaces under
io.github.rthadani.bff.* or extend the convenience base classes — pick per
extension. See extensions.md for the full API.
import bff.executor.BaseTransformer;
import bff.executor.BaseResolver;
import bff.validator.BaseValidator;
import io.github.rthadani.bff.BffContextEnricher;
import io.github.rthadani.bff.BffRetryHook;
import io.github.rthadani.bff.CacheStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
public class BffExtensions {
@Autowired StringRedisTemplate redis;
@Autowired CmapTokenService tokens;
public BffContextEnricher customerEnricher() { return new CustomerEnricher(redis); }
public BaseValidator orderValidator() { return new OrderValidator(); }
public BaseTransformer warningsTransformer(){ return new WarningsTransformer(); }
public BaseResolver userProfileResolver(){ return new UserProfileResolver(); }
public BffRetryHook tokenRefreshHook() { return new TokenRefreshHook(tokens); }
public CacheStore redisCacheStore() { return new RedisCacheStore(redis); }
static class WarningsTransformer extends BaseTransformer {
@Override
protected Map<String, Object> doTransform(
Map<String, Object> args,
Map<String, StepResult> chainCtx,
Map<String, Object> output) {
List<String> warnings = new ArrayList<>();
if (chainCtx.get("notify_user").isError()) {
warnings.add("Notification could not be sent");
}
output.put("warnings", warnings);
return output;
}
}
static class OrderValidator extends BaseValidator {
@Override
protected List<String> doValidate(Map<String, Object> args, Map<String, Object> ctx) {
List<String> errors = new ArrayList<>();
Double amount = (Double) args.get("amount");
if (amount != null && amount > 10000) {
errors.add("Orders above 10000 require manual approval");
}
return errors;
}
}
static class UserProfileResolver extends BaseResolver {
@Override
protected ResolverResult doResolve(Map<String, Object> args, Map<String, Object> ctx) {
return ResolverResult.ok(Map.of("fullName", "Alice", "email", "alice@example.com"));
}
}
static class CustomerEnricher implements BffContextEnricher {
private final StringRedisTemplate redis;
CustomerEnricher(StringRedisTemplate redis) { this.redis = redis; }
@Override public Map<String, Object> enrich(Map<String, Object> ctx) {
String subject = JwtUtil.subject((String) ctx.get("authorization"));
Object cust = redis.opsForHash().get("user:" + subject, "customerId");
return cust == null ? null : Map.of("customerId", cust.toString());
}
}
static class TokenRefreshHook implements BffRetryHook {
private final CmapTokenService tokens;
TokenRefreshHook(CmapTokenService tokens) { this.tokens = tokens; }
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> beforeRetry(Map<String, Object> failureContext) {
Map<String, Object> requestCtx = (Map<String, Object>) failureContext.get("request-ctx");
Map<String, Object> next = new HashMap<>(requestCtx);
next.put("authorization", "Bearer " + tokens.refresh());
return next;
}
}
static class RedisCacheStore implements CacheStore {
private final StringRedisTemplate redis;
RedisCacheStore(StringRedisTemplate redis) { this.redis = redis; }
@Override public Object get(String key) {
return redis.opsForValue().get(key);
}
@Override public void put(String key, Object value, long ttlMs) {
redis.opsForValue().set(key, value.toString(), Duration.ofMillis(ttlMs));
}
@Override public void invalidate(String key) {
redis.delete(key);
}
}
}
BFF has no built-in auth — it never sees JWTs and never mints tokens. Put
Spring Security's SecurityFilterChain in front of the servlet path:
@Bean
public SecurityFilterChain bffSecurity(HttpSecurity http) throws Exception {
return http
.securityMatcher("/graphql", "/graphiql")
.oauth2ResourceServer(o -> o.jwt())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/graphiql").permitAll()
.anyRequest().authenticated())
.build();
}
The authorization header (and x-request-id, x-correlation-id) are
forwarded to every downstream HTTP step automatically. See
forward_headers in the spec reference if you need to widen or
narrow that list.
Before 0.2.0 extensions were registered globally via Bff.registerValidator,
Bff.registerCache, etc. Those methods are gone. Replace each call with the
corresponding builder method on BffConfig.builder() and pass the built
config to Bff.createHandler(spec, config) or
Bff.createServlet(spec, config). The bff.*/register-*! Clojure functions
and the process-wide atoms are also gone.
See extensions.md for full documentation on each extension type.
Can you improve this documentation? These fine people already did:
Rohit Thadani & rthadaniEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |