Back to Blog

Building a Scalable REST API with Spring Boot

3 min read
javaspring-bootbackendapi

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!