Link copied to clipboard!
BlogForge AI

15 Rules Every Java Developer Should Follow for Spring Boot REST APIs

15 Rules Every Java Developer Should Follow for Spring Boot REST API Best Practices

Building a simple REST controller in Spring Boot takes less than two minutes. Annotate a class with @RestController, add a @GetMapping, wire up a repository, and you have an endpoint that returns JSON.

However, taking that endpoint from a local prototype to a production-ready, secure, and maintainable backend service requires mastering essential Spring Boot REST API best practices. In production, unvetted endpoints leak sensitive fields, throw unhandled 500 Internal Server Error stack traces, crash under unpaginated database queries, and create tight coupling between external consumers and internal database schemas.

Whether you are building enterprise backend services, designing microservices, or preparing for Java Spring Boot REST API interview questions, these 15 rules will help you design Spring Boot REST APIs that are production-grade by default.

1. Strictly Respect HTTP Method Semantics

The Engineering Problem

Treating all HTTP endpoints as generic transport channels (or relying almost exclusively on GET and POST) destroys the uniform interface constraint of REST. It prevents edge caches, API gateways, and client SDKs from making safe assumptions about retries, idempotency, and side effects.

Bad Approach

Using POST for fetching data or embedding operational verbs in the URI:

Java
// BAD: Mutating state via GET, using POST for retrieval, verb-based URIs
@GetMapping("/users/delete/{id}")
public void deleteUser(@PathVariable Long id) { ... }

@PostMapping("/users/get-by-email")
public User getUserByEmail(@RequestBody EmailRequest request) { ... }

Recommended Approach

Align your controller mapping directly with standard HTTP verb semantics:

Java
// RECOMMENDED: Clean separation by HTTP semantics
@GetMapping("/users")
public ResponseEntity> getUsers(Pageable pageable) { ... }

@PostMapping("/users")
public ResponseEntity createUser(@Valid @RequestBody CreateUserRequest request) { ... }

@PutMapping("/users/{id}")
public ResponseEntity replaceUser(@PathVariable Long id, @Valid @RequestBody UpdateUserRequest request) { ... }

@PatchMapping("/users/{id}")
public ResponseEntity updateUserStatus(@PathVariable Long id, @Valid @RequestBody PatchUserStatusRequest request) { ... }

@DeleteMapping("/users/{id}")
public ResponseEntity deleteUser(@PathVariable Long id) { ... }

Why It Matters in Production

  • GET: Must be safe (no side effects) and idempotent. Caches (like Cloudflare or Varnish) and browsers will cache these safely.
  • PUT: Fully replaces a resource and must be idempotent. Retrying a failed PUT request will not duplicate state.
  • PATCH: Applies partial updates.
  • DELETE: Must be idempotent. Deleting an already deleted resource should return 204 No Content or 404 Not Found, without breaking system state.
Interview Tip: Expect questions on the difference between PUT and PATCH, and which HTTP methods are safe versus idempotent.

2. Model Resources, Not Actions, in URLs

The Engineering Problem

Exposing RPC-style action names in URLs (e.g., /create-user, /update-order) creates inconsistent, fragile API paths that are difficult to discover, version, and document.

Bad Approach

HTTP / Text
POST /api/v1/createCustomer
POST /api/v1/updateCustomerStatus?id=10
GET  /api/v1/getAllActiveOrders

Recommended Approach

Use plural nouns to represent resource collections, and leverage nested paths to represent hierarchical relationships:

HTTP / Text
GET    /api/v1/customers         # Fetch a collection of customers
POST   /api/v1/customers         # Create a new customer
GET    /api/v1/customers/42      # Fetch a specific customer
GET    /api/v1/customers/42/orders # Fetch orders belonging to customer 42

3. Return Accurate, Fine-Grained HTTP Status Codes

The Engineering Problem

Returning 200 OK for every response—even when an operation fails—forces consumers to parse the JSON body to determine whether a request actually succeeded. Conversely, leaking raw 500 Internal Server Error exceptions signals unhandled application faults.

Recommended Approach

Leverage Spring's ResponseEntity or @ResponseStatus to return precise HTTP status codes:

Java
@PostMapping("/users")
public ResponseEntity createUser(@Valid @RequestBody CreateUserRequest request) {
    UserResponse created = userService.createUser(request);
    
    URI location = ServletUriComponentsBuilder.fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(created.id())
            .toUri();
            
    return ResponseEntity.created(location).body(created); // 201 Created with Location header
}

@DeleteMapping("/users/{id}")
public ResponseEntity deleteUser(@PathVariable Long id) {
    userService.deleteUser(id);
    return ResponseEntity.noContent().build(); // 204 No Content
}
Status Code Meaning Usage
200 OK Success Standard response for successful GET, PUT, or PATCH.
201 Created Resource Created Response for successful POST. Should include Location header.
204 No Content Success (No Body) Response for successful DELETE or empty updates.
400 Bad Request Client Error Malformed JSON, payload syntax errors, validation failures.
401 Unauthorized Unauthenticated Missing or invalid authentication token (JWT).
403 Forbidden Unauthorized Authenticated user lacks sufficient roles/authorities.
404 Not Found Not Found Target resource URI does not exist.
409 Conflict State Conflict Duplicate unique key (e.g., duplicate email address).

4. Decouple Database Entities from DTOs using Java Records

The Engineering Problem

Exposing JPA @Entity instances directly in controllers introduces critical security vulnerabilities, circular reference crashes, and tightly couples external API clients to internal database schemas.

Recommended Approach

Use immutable Java Records DTO Spring Boot patterns for incoming requests and outgoing responses:

Java
public record CreateUserRequest(
    @NotBlank(message = "Username is required")
    @Size(min = 3, max = 50)
    String username,

    @NotBlank(message = "Email is required")
    @Email(message = "Invalid email format")
    String email,

    @NotBlank(message = "Password is required")
    @Size(min = 8, message = "Password must be at least 8 characters")
    String password
) {}

public record UserResponse(
    Long id,
    String username,
    String email,
    Instant createdAt
) {}

5. Validate Every Request Payload Declaratively

The Engineering Problem

Imperative input validation pollutes business logic, leads to code duplication, and makes error responses inconsistent across endpoints.

Recommended Approach

Combine jakarta.validation annotations on your request Records with @Valid in your controller endpoints for clean Spring Boot REST API pagination validation standards:

Java
@PostMapping
public ResponseEntity createOrder(@Valid @RequestBody CreateUserRequest request) {
    OrderResponse response = orderService.createOrder(request);
    return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

6. Implement Global Exception Handling with @RestControllerAdvice

The Engineering Problem

Scatter-gathering try-catch blocks across controller methods produces duplicated, unmaintainable boilerplate and leaks raw stack traces.

Recommended Approach

Centralize exception handling into a single @RestControllerAdvice component:

Java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity handleResourceNotFound(
            ResourceNotFoundException ex, HttpServletRequest request) {
        log.warn("Resource not found: {}", ex.getMessage());
        
        ErrorResponse error = new ErrorResponse(
                HttpStatus.NOT_FOUND.value(),
                "NOT_FOUND",
                ex.getMessage(),
                request.getRequestURI(),
                Instant.now(),
                null
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
}

7. Adopt the RFC 9457 Problem Details Standard

The Engineering Problem

Every team returning a custom JSON format for errors forces frontends and downstream API consumers to write custom parser logic.

Recommended Approach

Adhere to RFC 9457 (Problem Details for HTTP APIs), leveraging advanced Spring Boot 3 exception handling ProblemDetail configurations:

Java
@RestControllerAdvice
public class GlobalProblemDetailsHandler {

    @ExceptionHandler(InsufficientInventoryException.class)
    public ProblemDetail handleInsufficientInventory(InsufficientInventoryException ex) {
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
                HttpStatus.CONFLICT, 
                ex.getMessage()
        );
        problemDetail.setTitle("Insufficient Inventory");
        problemDetail.setType(URI.create("https://api.example.com/errors/insufficient-inventory"));
        problemDetail.setProperty("sku", ex.getSku());
        problemDetail.setProperty("requestedQuantity", ex.getRequestedQuantity());
        problemDetail.setProperty("availableQuantity", ex.getAvailableQuantity());
        return problemDetail;
    }
}

8. Mandate Pagination and Guard Limits for Collection Endpoints

The Engineering Problem

Executing unpaginated queries in production datasets containing millions of rows causes high DB memory pressure, CPU spikes, huge JSON payloads, and OutOfMemoryError crashes.

Recommended Approach

Always accept Spring Data's Pageable parameter on collection endpoints and enforce maximum limit boundaries:

Java
@GetMapping
public ResponseEntity> getOrders(
        @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
    
    Pageable cappedPageable = PageRequest.of(
            pageable.getPageNumber(),
            Math.min(pageable.getPageSize(), 100), // Cap max page size to 100
            pageable.getSort()
    );
    
    return ResponseEntity.ok(orderService.getOrdersForCurrentCustomer(cappedPageable));
}

9. Separate Filtering, Searching, and Sorting Parameters

Pass query parameters cleanly and map them into Spring Data JPA Specification or Querydsl implementations to avoid endpoint route explosions.

10. Implement API Versioning Explicitly

Use URI path versioning (e.g., /api/v1/customers vs /api/v2/customers) as the industry standard for contract changes.

11. Enforce Stateless, JWT-Based Authentication

Configure Spring Security with SessionCreationPolicy.STATELESS and validate JSON Web Tokens using custom filters.

12. Apply Granular Role- and Authority-Based Authorization

Enable method-level security with @EnableMethodSecurity and annotate endpoints with @PreAuthorize rules.

13. Protect Sensitive Data in Logs and Storage

Exclude passwords, tokens, and PII from automated logging frameworks using annotations like @ToString.Exclude.

14. Expose Observability via Spring Boot Actuator and Structured Logging

Include spring-boot-starter-actuator and use Mapped Diagnostic Context (MDC) for distributed request tracing.

15. Automate OpenAPI Documentation with Springdoc

Include springdoc-openapi-starter-webmvc-ui to auto-generate OpenAPI 3 specifications and Swagger UI directly from your code.

Conclusion

Designing enterprise-grade production ready Spring Boot REST API architectures requires moving beyond basic @RestController prototypes. By following these Spring Boot REST API best practices, you ensure your backend is robust and secure.

REFERENCES:

REST API INTERVIEW QUESTIONS 

JWT AUTHENTICATION  

FOOD_MICROSERVICES_PROJECT

JAVA RECORDS

Written by Chagalakonda Sandeep Krishna

Senior Java, Spring Boot & AI Engineer. Architecting modern enterprise backend systems.