AgentDBDocs

Java and Spring Boot

Call AgentDB from a Spring Boot controller with RestClient (Spring Framework 6.1 and later). The key stays in configuration. Your frontend calls the controller.

Not run in CI. The Java code is not run in the test suite. It makes the same AgentDB calls as the starters that the smoke test runs.

#Where the key lives

In environment variables, read through application.properties:

properties
# application.properties
agentdb.url=${AGENTDB_API_URL:https://app.agentdb.link}
agentdb.workspace=${AGENTDB_WORKSPACE}
agentdb.token=${AGENTDB_TOKEN}

Do not put the real key in application.properties in source control. Do not copy it into a Thymeleaf template or a JavaScript bundle.

#The AgentDB client

Java
@Configuration
class AgentDbConfig {
    @Bean
    RestClient agentdb(@Value("${agentdb.url}") String url,
                       @Value("${agentdb.workspace}") String workspace,
                       @Value("${agentdb.token}") String token) {
        return RestClient.builder()
            .baseUrl(url + "/v1/workspaces/" + workspace)
            .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token)
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();
    }
}

#The backend route

Java
@RestController
@RequestMapping("/api/contacts")
class ContactController {
    private final RestClient agentdb;

    ContactController(RestClient agentdb) { this.agentdb = agentdb; }

    @GetMapping
    Map<String, Object> list(@RequestParam(defaultValue = "") String q) {
        String term = q.strip().length() > 100 ? q.strip().substring(0, 100) : q.strip();
        List<Map<String, Object>> where = term.isEmpty() ? List.of() : List.of(Map.of("field", "name", "op", "contains", "value", term));
        Map<String, Object> query = Map.of(
            "object", "contact",
            "where", Map.of("all", where),
            "sort", List.of(Map.of("field", "name", "order", "asc")),
            "limit", 50);
        JsonNode records = agentdb.post().uri("/query").body(query).retrieve().body(JsonNode.class).path("data").path("records");
        List<Map<String, Object>> contacts = new ArrayList<>();
        for (JsonNode r : records) contacts.add(Map.of("id", r.path("id").asText(), "name", r.path("data").path("name").asText()));
        return Map.of("contacts", contacts);
    }

    @PostMapping
    ResponseEntity<Map<String, Object>> create(@RequestBody Map<String, String> body,
                                               @RequestHeader(value = "X-Submit-Id", required = false) String submitId) {
        String name = body.getOrDefault("name", "").strip();
        if (name.isEmpty() || name.length() > 200) return ResponseEntity.badRequest().body(Map.of("error", "A contact needs a name."));
        String key = "spring." + (submitId != null ? submitId.replaceAll("[^A-Za-z0-9-]", "") : UUID.randomUUID().toString());
        JsonNode rec = agentdb.post().uri("/records").header("Idempotency-Key", key)
            .body(Map.of("object", "contact", "data", Map.of("name", name)))
            .retrieve().body(JsonNode.class).path("data");
        return ResponseEntity.status(201).body(Map.of("contact", Map.of("id", rec.path("id").asText(), "name", name)));
    }

    @ExceptionHandler(HttpClientErrorException.class)
    ResponseEntity<Map<String, Object>> agentdbError(HttpClientErrorException e) {
        if (e.getStatusCode().value() == 409) return ResponseEntity.status(409).body(Map.of("error", "Someone changed it first."));
        if (e.getStatusCode().value() == 400) return ResponseEntity.badRequest().body(Map.of("error", "Check the input."));
        return ResponseEntity.status(502).body(Map.of("error", "The data service is not available."));
    }
}

#The frontend

Serve the page from src/main/resources/static/. It calls /api/contacts on the same origin, like the page in Plain HTML and JavaScript. If you use Spring Security with sessions, keep CSRF protection on for POST routes.

#Loading, empty and error states

The controller answers { "error": "..." } with 400, 409 or 502. The page shows "Loading…" while it waits, "No contacts found" for an empty list, and the error message otherwise.

#Live updates

Return an SseEmitter from a controller, and run one scheduled task that follows the change feed (GET /changes?after=<seq>&wait=1) and sends to every emitter. See the live dashboard starter and Live data.