DEV Community

Rifki Andriyanto
Rifki Andriyanto

Posted on

10 Spring Boot REST API Best Practices That'll Save Your Sanity

Hey developers! πŸ‘‹

So you're building REST APIs with Spring Boot? Cool! But are you doing it right? I've seen way too many APIs that work but... let's just say they could be better. A lot better.

Today I'm sharing 10 best practices that'll make your Spring Boot APIs cleaner, more maintainable, and actually enjoyable to work with. Let's dive in!

1. Use Consistent and RESTful Resource Naming

❌ Don't do this:

@RestController
@RequestMapping("/user") // Singular? Nope!
public class UserController {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

βœ… Do this instead:

@RestController
@RequestMapping("/users") // Always plural!
public class UserController {

    @GetMapping // Just this, no "/getAllUsers"
    public List<User> getAllUsers() {
        return userService.getAllUsers();
    }

    @PostMapping // Not "/createUser"
    public ResponseEntity<?> createUser(@RequestBody UserRequest request) {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

Why? Keep it simple and follow REST conventions. Your API consumers will thank you.

2. Return the Correct HTTP Status Codes

Stop returning 200 OK for everything! Here's what you should actually return:

@PostMapping
public ResponseEntity<?> createUser(@RequestBody UserRequest request) {
    User user = userService.createUser(request);
    return ResponseEntity
        .status(HttpStatus.CREATED) // 201, not 200!
        .build();
}
Enter fullscreen mode Exit fullscreen mode

Quick reference:

  • 200 - OK (for successful GET, PUT)
  • 201 - Created (for successful POST)
  • 204 - No Content (for successful DELETE)
  • 400 - Bad Request (validation errors)
  • 404 - Not Found
  • 500 - Internal Server Error

3. Never Expose Your Entities (Use DTOs!)

❌ This is dangerous:

@PostMapping
public User createUser(@RequestBody User user) { // DON'T!
    return userService.save(user);
}
Enter fullscreen mode Exit fullscreen mode

Why? Because you might accidentally expose sensitive data like passwords!

βœ… Use DTOs instead:

// Request DTO
public record UserRequest(
    String name,
    String email,
    String password
) {}

// Response DTO
public record UserResponse(
    Integer id,
    String name,
    String email
    // No password here!
) {}

@PostMapping
public UserResponse createUser(@RequestBody UserRequest request) {
    return userService.createUser(request);
}
Enter fullscreen mode Exit fullscreen mode

4. Use Bean Validation (Stop Writing If Statements!)

❌ Please don't do this:

@PostMapping
public ResponseEntity<?> createUser(@RequestBody UserRequest request) {
    if (request.getName().isBlank()) {
        throw new IllegalArgumentException("Name is required");
    }
    if (request.getEmail().isBlank()) {
        throw new IllegalArgumentException("Email is required");
    }
    // More if statements... 😡
}
Enter fullscreen mode Exit fullscreen mode

βœ… Use validation annotations:

public record UserRequest(
    @NotBlank(message = "Name is required")
    String name,

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

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

@PostMapping
public ResponseEntity<?> createUser(@Valid @RequestBody UserRequest request) {
    // Validation happens automatically!
    return userService.createUser(request);
}
Enter fullscreen mode Exit fullscreen mode

5. Separate Your Concerns (Controller β†’ Service β†’ Repository)

Don't put business logic in your controllers!

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService; // Inject service

    @PostMapping
    public ResponseEntity<?> createUser(@Valid @RequestBody UserRequest request) {
        UserResponse response = userService.createUser(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }
}

@Service
public class UserService {

    private final UserRepository userRepository; // Inject repository

    public UserResponse createUser(UserRequest request) {
        // Business logic goes here
        User user = new User(request.name(), request.email());
        User savedUser = userRepository.save(user);
        return new UserResponse(savedUser.getId(), savedUser.getName(), savedUser.getEmail());
    }
}
Enter fullscreen mode Exit fullscreen mode

6. Always Use Pagination

Never return all records at once. Seriously, never.

@GetMapping
public Page<UserResponse> getAllUsers(Pageable pageable) {
    return userService.getAllUsers(pageable);
}
Enter fullscreen mode Exit fullscreen mode

Your API calls will look like:

GET /users?page=0&size=10&sort=name,asc
Enter fullscreen mode Exit fullscreen mode

7. Centralize Exception Handling

❌ Don't handle exceptions everywhere:

@PostMapping
public ResponseEntity<?> createUser(@RequestBody UserRequest request) {
    try {
        // ...
    } catch (Exception e) {
        return ResponseEntity.badRequest().body("Something went wrong");
    }
}
Enter fullscreen mode Exit fullscreen mode

βœ… Use @ControllerAdvice:

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> handleValidationErrors(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error -> 
            errors.put(error.getField(), error.getDefaultMessage())
        );
        return ResponseEntity.badRequest().body(errors);
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<?> handleUserNotFound(UserNotFoundException ex) {
        return ResponseEntity.notFound().build();
    }
}
Enter fullscreen mode Exit fullscreen mode

8. Implement Security (Don't Skip This!)

I'm not going to implement it here, but please secure your APIs! Options include:

  • JWT tokens
  • OAuth2
  • Basic authentication
  • API keys

Use Spring Security – it's your friend.

9. Version Your APIs

Always version your APIs from day one:

@RestController
@RequestMapping("/api/v1/users") // Version it!
public class UserController {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

When you need to make breaking changes, create /api/v2/users and keep v1 running until everyone migrates.

10. Document Your APIs

Use tools like:

  • Swagger/OpenAPI (most popular)
  • Spring REST Docs
  • Postman Collections

Your future self (and your teammates) will thank you.

Bonus Tip: Test Your APIs!

Write integration tests for your endpoints:

@SpringBootTest
@AutoConfigureTestDatabase
class UserControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldCreateUser() {
        UserRequest request = new UserRequest("John", "john@example.com", "password123");

        ResponseEntity<UserResponse> response = restTemplate.postForEntity(
            "/api/v1/users", request, UserResponse.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().name()).isEqualTo("John");
    }
}
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

These practices might seem like "extra work" at first, but trust me – they'll save you hours of debugging and refactoring later. Your APIs will be:

  • βœ… More maintainable
  • βœ… Easier to test
  • βœ… More secure
  • βœ… Better documented
  • βœ… Actually enjoyable to work with

What's your biggest Spring Boot API pain point? Drop a comment below – I'd love to help!

Top comments (0)