Building RESTful APIs with Spring Boot

A comprehensive guide to creating RESTful APIs using Spring Boot with best practices and examples

Building RESTful APIs with Spring Boot

This guide demonstrates how to create production-ready RESTful APIs using Spring Boot, including best practices, error handling, and documentation.

Video Tutorial

Learn more about building RESTful APIs with Spring Boot in this comprehensive video tutorial:

Prerequisites

  • JDK 17 or later
  • Maven 3.6+
  • Your favorite IDE (IntelliJ IDEA, Eclipse, or VS Code)

Project Setup

Create a new Spring Boot project using Spring Initializr with these 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-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.3.0</version>
    </dependency>
</dependencies>

Basic REST Controller

Here’s a simple REST controller example:

@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
    
    private final ProductService productService;
    
    public ProductController(ProductService productService) {
        this.productService = productService;
    }
    
    @GetMapping
    public List<Product> getAllProducts() {
        return productService.findAll();
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<Product> getProduct(@PathVariable Long id) {
        return productService.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }
    
    @PostMapping
    public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
        Product saved = productService.save(product);
        URI location = ServletUriComponentsBuilder
                .fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(saved.getId())
                .toUri();
        return ResponseEntity.created(location).body(saved);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<Product> updateProduct(
            @PathVariable Long id,
            @Valid @RequestBody Product product) {
        return productService.update(id, product)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        if (productService.delete(id)) {
            return ResponseEntity.noContent().build();
        }
        return ResponseEntity.notFound().build();
    }
}

Data Model

@Entity
@Table(name = "products")
public class Product {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "Product name is required")
    private String name;
    
    @NotNull(message = "Price is required")
    @Positive(message = "Price must be positive")
    private BigDecimal price;
    
    @NotBlank(message = "Description is required")
    private String description;
    
    // Getters, setters, and constructors
}

Service Layer

@Service
@Transactional
public class ProductService {
    
    private final ProductRepository productRepository;
    
    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }
    
    public List<Product> findAll() {
        return productRepository.findAll();
    }
    
    public Optional<Product> findById(Long id) {
        return productRepository.findById(id);
    }
    
    public Product save(Product product) {
        return productRepository.save(product);
    }
    
    public Optional<Product> update(Long id, Product product) {
        return productRepository.findById(id)
                .map(existing -> {
                    existing.setName(product.getName());
                    existing.setPrice(product.getPrice());
                    existing.setDescription(product.getDescription());
                    return productRepository.save(existing);
                });
    }
    
    public boolean delete(Long id) {
        return productRepository.findById(id)
                .map(product -> {
                    productRepository.delete(product);
                    return true;
                })
                .orElse(false);
    }
}

Global Exception Handling

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, List<String>>> handleValidationErrors(
            MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.toList());
        return ResponseEntity
                .badRequest()
                .body(Map.of("errors", errors));
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, String>> handleGeneralErrors(Exception ex) {
        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Map.of("error", "An unexpected error occurred"));
    }
}

OpenAPI Documentation

Add this configuration to enable Swagger UI:

@Configuration
public class OpenApiConfig {
    
    @Bean
    public OpenAPI springShopOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("Product API")
                        .description("Spring Boot REST API for Product Management")
                        .version("v1.0.0")
                        .license(new License()
                                .name("Apache 2.0")
                                .url("http://springdoc.org")));
    }
}

Access the Swagger UI at: http://localhost:8080/swagger-ui.html

Testing

@SpringBootTest
@AutoConfigureMockMvc
class ProductControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Test
    void whenCreateProduct_thenStatus201() throws Exception {
        Product product = new Product();
        product.setName("Test Product");
        product.setPrice(new BigDecimal("99.99"));
        product.setDescription("Test Description");
        
        mockMvc.perform(post("/api/v1/products")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(product)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.name").value("Test Product"));
    }
}

Best Practices

  1. Versioning

    • Use URL versioning (e.g., /api/v1/products)
    • Consider using headers or content negotiation for more flexibility
  2. Security

    • Always validate input
    • Use HTTPS in production
    • Implement proper authentication and authorization
  3. Performance

    • Use pagination for large datasets
    • Implement caching where appropriate
    • Consider using async operations for long-running tasks
  4. Documentation

    • Use OpenAPI/Swagger for API documentation
    • Document all error responses
    • Include example requests/responses
  5. Error Handling

    • Implement global exception handling
    • Return appropriate HTTP status codes
    • Provide meaningful error messages

Common Use Cases

Pagination

@GetMapping
public Page<Product> getProducts(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "name") String sort) {
    
    return productService.findAll(
        PageRequest.of(page, size, Sort.by(sort)));
}

Filtering

@GetMapping("/search")
public List<Product> searchProducts(
        @RequestParam(required = false) String name,
        @RequestParam(required = false) BigDecimal minPrice,
        @RequestParam(required = false) BigDecimal maxPrice) {
    
    return productService.searchProducts(name, minPrice, maxPrice);
}

Caching

@GetMapping("/{id}")
@Cacheable(value = "products", key = "#id")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
    return productService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
}

Conclusion

This guide covered the basics of building RESTful APIs with Spring Boot. Remember to:

  • Follow REST principles
  • Implement proper error handling
  • Document your API
  • Write comprehensive tests
  • Consider security implications
  • Follow performance best practices

For more advanced topics, check out our other Spring Boot guides: