Building a Scalable REST API with Spring Boot
Building a Scalable REST API with Spring Boot
In this post, I'll walk through building a production-ready REST API using Spring Boot, covering everything from project setup to deployment best practices.
Project Setup
First, let's set up a new Spring Boot project with the necessary dependencies:
// pom.xml dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
Creating the Domain Model
Let's create a simple User entity:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Email
private String email;
@NotBlank
@Size(min = 2, max = 100)
private String name;
@CreatedDate
private LocalDateTime createdAt;
// Getters and setters
}
Building the REST Controller
Here's a well-structured controller with proper error handling:
@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public ResponseEntity<List<UserDTO>> getAllUsers() {
List<UserDTO> users = userService.findAll();
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody CreateUserRequest request) {
UserDTO user = userService.create(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(user.getId())
.toUri();
return ResponseEntity.created(location).body(user);
}
}
Best Practices
1. Use DTOs for Request/Response
Never expose your entities directly. Use Data Transfer Objects (DTOs):
public record UserDTO(
Long id,
String email,
String name,
LocalDateTime createdAt
) {}
public record CreateUserRequest(
@NotBlank @Email String email,
@NotBlank @Size(min = 2, max = 100) String name
) {}
2. Implement Global Exception Handling
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(
MethodArgumentNotValidException ex
) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.toList());
return ResponseEntity
.badRequest()
.body(new ErrorResponse("Validation failed", errors));
}
}
3. Add API Documentation
Use SpringDoc OpenAPI for automatic documentation:
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI usersMicroserviceOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Users API")
.description("REST API for user management")
.version("1.0"));
}
}
Testing
Don't forget to write tests! Here's an example using MockMvc:
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldCreateUser() throws Exception {
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"email": "test@example.com",
"name": "Test User"
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.email").value("test@example.com"));
}
}
Performance Tips
Pro Tip: Use pagination for list endpoints to avoid loading large datasets into memory.
@GetMapping
public ResponseEntity<Page<UserDTO>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
Pageable pageable = PageRequest.of(page, size);
Page<UserDTO> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
Conclusion
Building a production-ready REST API requires attention to:
- Proper validation and error handling
- Using DTOs to decouple your API from internal models
- Comprehensive testing
- API documentation
- Performance optimization
In the next post, I'll cover how to add authentication and authorization to this API using Spring Security and JWT.
Have questions or suggestions? Feel free to reach out!