- Published on
- · 6 min read
Avoid these 9 common Spring Boot REST API Mistakes
- Authors
- Name
- Nguyễn Tạ Minh Trung
Introduction
Building REST APIs with Spring Boot is straightforward, but developers often make mistakes that can lead to performance issues, security vulnerabilities, and poor maintainability. In this blog, we’ll explore some common mistakes and how to avoid them.
1. Using Wrong HTTP Methods
Mistake:
Using incorrect HTTP methods for CRUD operations. For example, using POST instead of PUT for updating resources.
Spring Boot should follow RESTfull convention to maintain clean code and consistency.
Wrong Usage:
@PostMapping("/users/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
return ResponseEntity.ok(userService.updateUser(id, user));
}
Solution:
Following RESTful conventions:
GET
for retrieving dataPOST
for create new resourcesPUT
for updating resourcesDELETE
for removing resourcesPATCH
for partial updates
Correct Usage:
@PutMapping("/users/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
return ResponseEntity.ok(userService.updateUser(id, user));
}
2. Not Handling Exceptions Properly
Mistake:
Returning generic or stack trace errors instead of meaningful error responses is difficult to debug the problems, they also are potential vulnerabilities
Wrong Usage:
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id); // Throws exception if user not found
}
- Issue: This can result in an unhandled exception returning a generic 500 error with a stack trace.
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
try {
return userService.getUser(id);
} catch (Exception e) {
return null; // Bad practice
}
}
- Issue: Unclear exception will be caught
Solution:
Use @ControllerAdvice
to handle exceptions globally and return proper HTTP status codes.
Correct Usage:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleNotFoundException(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
}
3. Exposing Internal Implementation Details
Mistake:
Returning entire entity objects, exposing internal database structure or the sensitive information
Wrong Usage:
public class User {
private String name;
private String email;
private String password;
}
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
- Issue: This exposes database fields that might not be relevant to API consumers and also exposes the password which is sensitive user information
Solution:
Use DTOs (Data Transfer Objects) to control the API response:
Correct Usage:
public class User {
private String name;
private String email;
private String password;
}
public class UserDTO {
private String name;
private String email;
}
@GetMapping("/users/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserDTO(id));
}
4. Ignoring Pagination and Filtering
Mistake:
Returning all records without pagination or filtering. If the API is returning a huge amount of records, pagination and filtering is very important to reduce the performance issues and poor user experience
Wrong Usage:
@GetMapping("/users")
public List<User> getAllUsers() {
return userService.getAllUsers();
}
- Issue: This can lead to poor performance and excessive memory usage when dealing with large datasets.
Solution:
Use filtering and pagination. Example: Pageable
in Spring Data JPA to paginate result by page
and size
from the request.
Correct Usage:
@GetMapping("/users")
public ResponseEntity<Page<User>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id") String sortBy
) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
return userRepository.findAll(pageable);
}
5. Not Validating Request Bodies
Mistake:
Allowing invalid or incomplete data to be processed. This can lead to incorrect data being processed and stored, also can cause security vulnerabilities.
Wrong Usage:
public class User {
private String name;
private String email;
private String password;
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.createUser(user));
}
Solution:
Use validation annotations like @Valid
and @NotNull
.
Correct Usage:
public class User {
private String name;
private String email;
private String password;
}
public class UserRequest {
@NotNull
private String name;
@Email
private String email;
// Getters and setters
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody UserRequest request) {
return ResponseEntity.ok(userService.createUser(request));
}
6. Incorrect Response Status Codes
Mistake:
Do not return the Response Status Codes correctly.
Wrong Usage:
@PostMapping("/users")
public User createUser(@RequestBody User user) {
// Returns 200 OK for creation
return userService.createUser(user);
}
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
User user = userService.findById(id);
if (user == null) {
return new User(); // Returns empty object instead of 404
}
return user;
}
Solution:
Use ResponseStatus
or ResponseEntity
to set the Response Status Codes when returning the data
Correct Usage:
@PostMapping("/users")
@ResponseStatus(HttpStatus.CREATED)
public User createUser(@RequestBody User user) {
return userService.createUser(user);
}
Or
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
User createdUser = userService.createUser(user);
return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
}
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(user -> ResponseEntity.ok(user))
.orElse(ResponseEntity.notFound().build());
}
7. Inconsistent RESTful API Naming Convention
Mistake:
Each API has own its name, do not consistency with the related API, that makes the code harder to understand and use.
Wrong Usage:
@RestController
public class UserController {
@GetMapping("/getUsers")
public List<User> getUsers() { ... }
@PostMapping("/createNewUser")
public User createNewUser(@RequestBody User user) { ... }
@PutMapping("/updateUserDetails/{userId}")
public User updateUserDetails(@PathVariable Long userId) { ... }
}
Solution:
Use @RequestMapping
to unify API prefix, all related APIs will have the same prefix
Correct Usage:
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@GetMapping
public List<User> getUsers() { ... }
@PostMapping
public User createUser(@RequestBody User user) { ... }
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id) { ... }
}
8. Ignoring Security Best Practices
Mistake:
Not implementing authentication and authorization mechanisms.
Wrong Usage:
@GetMapping("/admin")
public String adminAccess() {
return "Admin Panel";
}
- Issue: No authentication, allowing unauthorized users to access admin resources.
Solution:
Use Spring Security for authentication and JWT for token-based security.
Correct Usage:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
9. Hardcoding Configuration Values
Mistake:
Storing database credentials, API keys, or other configurations in source code.
Wrong Usage:
String dbUrl = "jdbc:mysql://localhost:3306/mydb";
String dbUser = "root";
String dbPassword = "password";
- Issue: Hardcoded credentials pose security risks and make it difficult to change configurations.
Solution:
Use application.properties
or application.yaml
files to store the configuration. Additionally, we also use environment variables instead.
Correct Usage:
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DATABASE_USER}
spring.datasource.password=${DATABASE_PASSWORD}
Conclusion
Avoiding these common mistakes can lead to better performance, security, and maintainability for your Spring Boot REST APIs. By following best practices, such as using proper HTTP methods, handling exceptions properly, securing endpoints, and using DTOs, you can build robust and scalable APIs efficiently.