Skip to main content
All posts
JavaSpring BootBackendREST APIPostgreSQL

Building Production-Ready REST APIs with Spring Boot

From project structure and exception handling to database migrations, validation, and OpenAPI documentation — a pragmatic guide to Spring Boot APIs that hold up in production.

December 5, 20257 min read

A Spring Boot REST API that works in development and one that holds up in production are different things. This post focuses on the structural decisions that matter: layering, validation, error handling, Flyway migrations, and documentation — in a way that stays readable as the codebase grows.

Project structure

src/main/java/com/example/api/
  ├── config/          # Security, CORS, Jackson, OpenAPI
  ├── controller/      # @RestController — thin, delegates to services
  ├── service/         # Business logic
  ├── repository/      # Spring Data JPA interfaces
  ├── domain/          # @Entity classes
  ├── dto/             # Request/response records
  ├── exception/       # Custom exceptions + GlobalExceptionHandler
  └── mapper/          # MapStruct or manual DTOMapper

Keep controllers thin. A controller method should parse input, call one service method, and return a response. Business logic in controllers leads to test headaches.

Validation and error responses

Use Bean Validation on DTO records and handle MethodArgumentNotValidException centrally:

public record CreateOrderRequest(
    @NotBlank String packageId,
    @NotBlank @Email String email,
    @Positive Integer quantity
) {}
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        var errors = ex.getBindingResult().getFieldErrors()
            .stream()
            .map(e -> new FieldError(e.getField(), e.getDefaultMessage()))
            .toList();
        return ResponseEntity.badRequest().body(new ErrorResponse("VALIDATION_ERROR", errors));
    }

    @ExceptionHandler(EntityNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(EntityNotFoundException ex) {
        return ResponseEntity.status(404).body(new ErrorResponse(ex.getMessage(), List.of()));
    }
}

This gives clients consistent error shapes without scattering try-catch blocks through your controllers.

Database migrations with Flyway

Flyway keeps schema evolution versioned and auditable. Add it to pom.xml and Spring auto-configures it from spring.datasource:

-- V1__init.sql
CREATE TABLE orders (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id),
    package_id UUID NOT NULL REFERENCES packages(id),
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    amount_cents INTEGER NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Never edit an applied migration. Add a new V2__ file instead — this guarantees reproducibility across environments.

OpenAPI documentation

Springdoc auto-generates OpenAPI 3 from your annotations. Annotate meaningfully:

@Operation(summary = "Create a new order", description = "Initiates a Stripe Checkout session and returns the redirect URL.")
@ApiResponse(responseCode = "201", description = "Order created")
@ApiResponse(responseCode = "400", description = "Validation error")
@PostMapping("/orders")
public ResponseEntity<OrderResponse> createOrder(@Valid @RequestBody CreateOrderRequest request) { ... }

The resulting spec at /v3/api-docs can feed Postman, frontend clients, or a published docs site.

Performance basics

For read-heavy endpoints, add @Cacheable with Caffeine for in-memory caching. For heavy aggregations, consider a dedicated read model rather than complex JPQL. And always check N+1 queries — Hibernate's lazy loading is convenient but silently expensive. Use JOIN FETCH or @EntityGraph on collection associations you know you'll need.


Need a Spring Boot backend for your product — proper validation, Flyway migrations, JWT auth, and a tested service layer? Get in touch.

Ready to fix this for your business?

Fixed scope, fixed price, written handover - websites, full-stack apps, and DevOps pipelines delivered in weeks, not months.