<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Libre+Caslon+Text:ital,wght@0,400;0,700;1,400&family=Libre+Franklin:wght@400..800&family=JetBrains+Mono:wght@400;500;700&display=swap" />
Late edition Stop the presses

Suspect at large

A backend developer is fleeing the scene. The record is being set.

Setting the type Inking the plates Running the presses

Skip to the front page
Kathmandu, NepalThe Backend EditionEst. 2021

The personal record of a backend developer

Wednesday 5 August 2026Vol. ISelected works & notesPrice: one coffee
← Back to the dispatchesSpring Boot · 8 min read

Spring Boot

Building Scalable REST APIs with Spring Boot

A comprehensive guide to designing and implementing RESTful APIs using Spring Boot, covering best practices for authentication, validation, and error handling.

By · Backend Developer — Spring Boot & DevOps15 January 2024 · 8 min read

REST APIs are the backbone of modern web applications. In this comprehensive guide, we'll explore how to build scalable, maintainable REST APIs using Spring Boot.

Introduction

Spring Boot has revolutionized Java development by providing a convention-over-configuration approach that allows developers to quickly create production-ready applications. When it comes to building REST APIs, Spring Boot offers powerful features that make development both efficient and enjoyable.

Why Spring Boot for REST APIs?

  • Auto-configuration: Minimal setup required
  • Embedded servers: No need for external application servers
  • Production-ready features: Health checks, metrics, and monitoring
  • Rich ecosystem: Extensive library support
  • Developer experience: Hot reloading and excellent tooling

Project Setup

Let's start by creating a new Spring Boot project with the necessary dependencies.

Maven Dependencies

xml
<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>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

Application Properties

Configure your database connection and other settings:

properties
spring.datasource.url=jdbc:mysql://localhost:3306/api_db
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

Creating Entities

Let's create a User entity as an example:

java
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    @Email
    private String email;
    
    @Column(nullable = false)
    @Size(min = 2, max = 50)
    private String firstName;
    
    @Column(nullable = false)
    @Size(min = 2, max = 50)
    private String lastName;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
    
    // Constructors, getters, and setters
}

Repository Layer

Create a repository interface:

java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    List<User> findByFirstNameContainingIgnoreCase(String firstName);
}

Building Controllers

Now let's create a REST controller:

java
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = userService.getAllUsers();
        return ResponseEntity.ok(users);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = userService.getUserById(id);
        return ResponseEntity.ok(user);
    }
    
    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        User createdUser = userService.createUser(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(
            @PathVariable Long id, 
            @Valid @RequestBody User user) {
        User updatedUser = userService.updateUser(id, user);
        return ResponseEntity.ok(updatedUser);
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}

Service Layer

Implement the business logic:

java
@Service
@Transactional
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
    
    public User getUserById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
    }
    
    public User createUser(User user) {
        if (userRepository.findByEmail(user.getEmail()).isPresent()) {
            throw new EmailAlreadyExistsException("Email already exists: " + user.getEmail());
        }
        return userRepository.save(user);
    }
    
    public User updateUser(Long id, User userDetails) {
        User user = getUserById(id);
        user.setFirstName(userDetails.getFirstName());
        user.setLastName(userDetails.getLastName());
        user.setEmail(userDetails.getEmail());
        return userRepository.save(user);
    }
    
    public void deleteUser(Long id) {
        User user = getUserById(id);
        userRepository.delete(user);
    }
}

Error Handling

Implement global exception handling:

java
@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            System.currentTimeMillis()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
    
    @ExceptionHandler(EmailAlreadyExistsException.class)
    public ResponseEntity<ErrorResponse> handleEmailAlreadyExists(EmailAlreadyExistsException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.CONFLICT.value(),
            ex.getMessage(),
            System.currentTimeMillis()
        );
        return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(FieldError::getDefaultMessage)
            .collect(Collectors.toList());
            
        ErrorResponse error = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "Validation failed: " + String.join(", ", errors),
            System.currentTimeMillis()
        );
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }
}

Testing

Write comprehensive tests for your API:

java
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource(locations = "classpath:application-test.properties")
class UserControllerTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void shouldCreateUser() {
        User user = new User();
        user.setFirstName("John");
        user.setLastName("Doe");
        user.setEmail("[email protected]");
        
        ResponseEntity<User> response = restTemplate.postForEntity(
            "/api/users", user, User.class);
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().getEmail()).isEqualTo("[email protected]");
    }
    
    @Test
    void shouldReturnUserById() {
        User savedUser = userRepository.save(createTestUser());
        
        ResponseEntity<User> response = restTemplate.getForEntity(
            "/api/users/" + savedUser.getId(), User.class);
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody().getId()).isEqualTo(savedUser.getId());
    }
}

Best Practices

1. Use DTOs for Data Transfer

Create separate DTOs for request and response:

java
public class UserCreateRequest {
    @NotBlank
    @Email
    private String email;
    
    @NotBlank
    @Size(min = 2, max = 50)
    private String firstName;
    
    @NotBlank
    @Size(min = 2, max = 50)
    private String lastName;
    
    // getters and setters
}

2. Implement Pagination

java
@GetMapping
public ResponseEntity<Page<User>> getAllUsers(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "id") String sortBy) {
    
    Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
    Page<User> users = userService.getAllUsers(pageable);
    return ResponseEntity.ok(users);
}

3. Add API Documentation

Use Swagger/OpenAPI:

java
@RestController
@RequestMapping("/api/users")
@Tag(name = "User Management", description = "APIs for managing users")
public class UserController {
    
    @Operation(summary = "Get all users", description = "Retrieve a paginated list of all users")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "Successfully retrieved users"),
        @ApiResponse(responseCode = "500", description = "Internal server error")
    })
    @GetMapping
    public ResponseEntity<Page<User>> getAllUsers(/* parameters */) {
        // implementation
    }
}

Conclusion

Building scalable REST APIs with Spring Boot involves following best practices for project structure, error handling, validation, and testing. By leveraging Spring Boot's powerful features and following these guidelines, you can create robust, maintainable APIs that scale with your application's needs.

Key takeaways:

  • Use proper layered architecture (Controller → Service → Repository)
  • Implement comprehensive error handling
  • Add validation at multiple levels
  • Write thorough tests
  • Use DTOs for clean data transfer
  • Document your APIs properly
  • Consider pagination for large datasets

With these foundations in place, your Spring Boot REST APIs will be well-equipped to handle production workloads and future growth.

Backend developer specializing in Java and Spring Boot. Building scalable, reliable systems that power modern applications. This broadsheet is hand-set in Caslon and Franklin.

The desk

Tech stack

  • Java & Spring Boot
  • MySQL & PostgreSQL
  • Docker & Microservices
  • JWT & OAuth2
Case closed

System up since 2021 · Building backend systems · Learning new technologies · Contributing to open source

© 2026 Utsab Dahal · All rights reserved · Printed in Kathmandu