- Published on
Building an Enhanced API in Java with Spring Boot
Building an Enhanced API in Java with Spring Boot
To build a robust and maintainable REST API, we need to ensure clean code, efficient error handling, proper validation, and documentation. This post guides you through building an enhanced API using Java with Spring Boot, adding features such as DTOs, validation, custom exception handling, and Swagger documentation.
Table of Contents
- 1. Set Up and Additional Dependencies
- 2. Define the Entity Class
- 3. Create a Repository Interface
- 4. Define the DTOs and Validation
- 5. Implement a Custom Exception and Exception Handler
- 6. Add a Mapper Layer for DTO Conversion
- 7. Enhance the Service Layer
- 8. Enhance the Controller with DTOs, Validation, Pagination, and HATEOAS
- 9. Application Properties Configuration
- 10. Add Swagger/OpenAPI Documentation
- 11. Running and Testing the Enhanced API
- 12. Accessing Swagger UI
- Conclusion
- Further Reading and Resources
1. Set Up and Additional Dependencies
Set up a Spring Boot project using Spring Initializer with dependencies like Spring Web, Spring Data JPA, H2 Database, Lombok, ModelMapper, HATEOAS, and Springdoc OpenAPI for Swagger. Add the following dependencies to pom.xml
:
<!-- Spring Boot Starter Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Model Mapper for DTO Conversion -->
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.1</version>
</dependency>
<!-- Spring Boot Starter for HATEOAS -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<!-- Spring Boot Starter for Swagger/OpenAPI Documentation -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.7.0</version>
</dependency>
<!-- H2 Database for in-memory database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok for reducing boilerplate code -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2. Define the Entity Class
Define the User
entity class that maps to the database table. This class will represent the users in the system.
package com.example.api.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
@Entity
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
}
3. Create a Repository Interface
Create a repository interface that extends JpaRepository
for CRUD operations.
package com.example.api.repository;
import com.example.api.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
4. Define the DTOs and Validation
Create a UserDTO
class for data transfer and validation.
package com.example.api.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class UserDTO {
private Long id;
@NotBlank(message = "Name is mandatory")
@Size(min = 2, message = "Name should have at least 2 characters")
private String name;
@NotBlank(message = "Email is mandatory")
@Email(message = "Email should be valid")
private String email;
}
5. Implement a Custom Exception and Exception Handler
Create a custom exception class ResourceNotFoundException
.
package com.example.api.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
Enhance GlobalExceptionHandler
to handle custom exceptions.
package com.example.api.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<?> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
Map<String, String> errorDetails = new HashMap<>();
errorDetails.put("message", ex.getMessage());
errorDetails.put("details", request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleGlobalException(Exception ex, WebRequest request) {
Map<String, String> errorDetails = new HashMap<>();
errorDetails.put("message", "An error occurred");
errorDetails.put("details", request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
6. Add a Mapper Layer for DTO Conversion
Create a UserMapper
class for converting between User
entities and UserDTOs
using ModelMapper.
package com.example.api.mapper;
import com.example.api.dto.UserDTO;
import com.example.api.model.User;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class UserMapper {
@Autowired
private ModelMapper modelMapper;
public UserDTO toDto(User user) {
return modelMapper.map(user, UserDTO.class);
}
public User toEntity(UserDTO userDTO) {
return modelMapper.map(userDTO, User.class);
}
}
7. Enhance the Service Layer
Add more business logic and handle exceptions properly in the UserService
class.
package com.example.api.service;
import com.example.api.exception.ResourceNotFoundException;
import com.example.api.model.User;
import com.example.api.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User createUser(User user) {
return userRepository.save(user);
}
public List<User> getAllUsers(int page, int size, String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
Page<User> usersPage = userRepository.findAll(pageable);
return usersPage.getContent();
}
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found with ID: " + id));
}
public void deleteUser(Long id) {
User user = getUserById(id); // This will throw an exception if not found
userRepository.delete(user);
}
}
8. Enhance the Controller with DTOs, Validation, Pagination, and HATEOAS
Enhance UserController
to use UserDTO
, handle validations, support pagination, and provide HATEOAS links.
package com.example.api.controller;
import com.example.api.dto.UserDTO;
import com.example.api.mapper.UserMapper;
import com.example.api.model.User;
import com.example.api.service.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService
userService;
@Autowired
private UserMapper userMapper;
@PostMapping
public ResponseEntity<EntityModel<UserDTO>> createUser(@Valid @RequestBody UserDTO userDTO) {
User user = userMapper.toEntity(userDTO);
User createdUser = userService.createUser(user);
UserDTO createdUserDTO = userMapper.toDto(createdUser);
EntityModel<UserDTO> resource = EntityModel.of(createdUserDTO);
Link selfLink = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(this.getClass()).getUserById(createdUser.getId())).withSelfRel();
resource.add(selfLink);
return new ResponseEntity<>(resource, HttpStatus.CREATED);
}
@GetMapping
public ResponseEntity<List<EntityModel<UserDTO>>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy) {
List<User> users = userService.getAllUsers(page, size, sortBy);
List<EntityModel<UserDTO>> userDTOs = users.stream()
.map(user -> {
UserDTO userDTO = userMapper.toDto(user);
return EntityModel.of(userDTO,
WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(this.getClass()).getUserById(user.getId())).withSelfRel());
})
.collect(Collectors.toList());
return new ResponseEntity<>(userDTOs, HttpStatus.OK);
}
@GetMapping("/{id}")
public ResponseEntity<EntityModel<UserDTO>> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
UserDTO userDTO = userMapper.toDto(user);
EntityModel<UserDTO> resource = EntityModel.of(userDTO);
Link selfLink = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(this.getClass()).getUserById(id)).withSelfRel();
resource.add(selfLink);
return new ResponseEntity<>(resource, HttpStatus.OK);
}
@DeleteMapping("/{id}")
public ResponseEntity<String> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return new ResponseEntity<>("User deleted successfully", HttpStatus.OK);
}
}
9. Application Properties Configuration
Configure the in-memory H2 database and other properties in src/main/resources/application.properties
.
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=update
10. Add Swagger/OpenAPI Documentation
To add Swagger documentation, configure application.properties
and add Swagger annotations to the controller.
# application.properties
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui.html
11. Running and Testing the Enhanced API
Run the application using the command:
mvn spring-boot:run
Test the API using Postman or cURL:
- Create a User:
curl -X POST http://localhost:8080/api/users -H "Content-Type: application/json" -d '{"name": "John Doe", "email": "[email protected]"}'
- Get All Users:
curl -X GET http://localhost:8080/api/users
12. Accessing Swagger UI
Visit http://localhost:8080/swagger-ui.html to see the Swagger UI for your API documentation and testing.
Conclusion
By following this guide, you've built a comprehensive REST API in Java with Spring Boot, incorporating advanced features and best practices. This API is ready for production-level applications with enhanced maintainability, scalability, and usability.