Medium intuit companies

How do you build a new API live in a Spring Boot codebase you were given in advance?

The 30 second answer

I start by restating the requirements and agreeing the request and response shapes, then follow the codebase's existing layers: a thin controller with validation, a service that owns the business logic and transactions, and a repository for data access, with DTOs at the edge. I add pagination with a capped page size, map errors centrally to clear responses, check security and rate limits, and write a test for each endpoint, explaining each choice as I go.

Before the round. Clone, build and run the project and its tests, and fix anything environment-related then, not on the call. Find one existing endpoint and trace it end to end. Note which conventions exist already: DTOs or entities in responses, a global exception handler, the security configuration, the test style. Reusing them is faster and shows you can work in someone else's code.

In the round, in this order.

  1. Clarify. Restate the two APIs, the request and response JSON, status codes, and edge cases (not found, duplicates, empty results, invalid input). Agree what "done" means.
  2. Layers. Keep the controller thin: it maps HTTP to a method call and validates input. The service holds business rules and the @Transactional boundary. The repository only talks to the database. Return DTOs, not JPA entities, so the API contract does not change when the table does and lazy-loading surprises stay out of serialization.
  3. Pagination. Accept page and size, cap size, use a stable sort, and return page metadata. Mention keyset pagination for large or frequently changing tables.
  4. Errors. Map exceptions in one @RestControllerAdvice, returning consistent bodies. Spring Framework 6 supports the RFC 9457 problem-details format through ProblemDetail and ResponseEntityExceptionHandler.
  5. Security and limits. Say how the endpoint is authenticated and authorized (for example a JWT resource server and a role or ownership check), and where rate limiting belongs: often at the API gateway, or with a token-bucket filter in the service that returns 429 Too Many Requests with Retry-After.
  6. Tests. A controller slice test with @WebMvcTest and MockMvc for status codes and validation, a service unit test for the rules, and if time allows an integration test with @SpringBootTest or @DataJpaTest, using Testcontainers for a real database.
public record OrderDto(Long id, String status, BigDecimal total) {}

@RestController
@RequestMapping("/api/orders")
class OrderController {
    private final OrderService service;
    OrderController(OrderService service) { this.service = service; }

    @GetMapping
    PageResponse<OrderDto> list(@RequestParam(defaultValue = "0") @Min(0) int page,
                                @RequestParam(defaultValue = "20") @Min(1) @Max(100) int size) {
        return service.list(PageRequest.of(page, size, Sort.by("id").descending()));
    }

    @GetMapping("/{id}")
    OrderDto get(@PathVariable Long id) { return service.get(id); }
}

@RestControllerAdvice
class ApiExceptionHandler extends ResponseEntityExceptionHandler {
    @ExceptionHandler(OrderNotFoundException.class)
    ProblemDetail notFound(OrderNotFoundException ex) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
    }
}

Here PageResponse, OrderService and OrderNotFoundException stand for the codebase's own types. On Spring Framework 6.1 and later, constraint annotations such as @Min on controller method parameters trigger built-in method validation, raising HandlerMethodValidationException; on older versions you add a class-level @Validated instead. If the controller keeps a class-level @Validated, validation runs through an AOP proxy instead, and the reference says to remove that annotation to use the built-in support. Either way, a request with size=1000000 gets a 400 instead of a million-row query.

Configuration through properties. The same round asked about pointing the app at a local LLM endpoint. Bind settings to a typed class instead of hard-coding them, so each environment changes only its properties:

@ConfigurationProperties(prefix = "llm")
public record LlmProperties(URI baseUrl, String model, Duration timeout) {}
// application.properties
// llm.base-url=http://localhost:8081
// llm.model=local-model
// llm.timeout=10s

Enable it with @ConfigurationPropertiesScan or @EnableConfigurationProperties(LlmProperties.class), and override per environment with profile-specific files or environment variables such as LLM_BASE_URL.

What the observers listen for. Small steps that keep the app compiling, a test run after each step, naming the trade-off when you choose one (for example "clamp or reject oversized pages? I'll reject with 400 so clients see the error"), and asking before changing shared code.

Sources (8)
  1. Spring Framework reference: Error responses and ProblemDetaildocs.spring.io
  2. Spring Framework reference: Controller advicedocs.spring.io
  3. RFC 9457: Problem Details for HTTP APIsrfc-editor.org
  4. Spring Framework reference: Validation in Spring MVC controllersdocs.spring.io
  5. Spring Framework reference: Java Bean Validationdocs.spring.io
  6. Spring Security reference: Authorize HTTP requestsdocs.spring.io
  7. Spring Boot reference: Testing Spring Boot applicationsdocs.spring.io
  8. Spring Boot reference: Externalized configurationdocs.spring.io