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
-
Versioning
- Use URL versioning (e.g.,
/api/v1/products
) - Consider using headers or content negotiation for more flexibility
- Use URL versioning (e.g.,
-
Security
- Always validate input
- Use HTTPS in production
- Implement proper authentication and authorization
-
Performance
- Use pagination for large datasets
- Implement caching where appropriate
- Consider using async operations for long-running tasks
-
Documentation
- Use OpenAPI/Swagger for API documentation
- Document all error responses
- Include example requests/responses
-
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: