Building and consuming RESTful web services in Java
REST (Representational State Transfer) is an architectural style for designing networked applications. Key principles:
| Method | Description | Idempotent |
|---|---|---|
| GET | Retrieve a resource | Yes |
| POST | Create a new resource | No |
| PUT | Update an existing resource (full update) | Yes |
| PATCH | Partially update a resource | No |
| DELETE | Remove a resource | Yes |
<!-- 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>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
// Product.java
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private String description;
private double price;
// Constructors, getters and setters
}
// ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByNameContaining(String name);
}
// ProductController.java
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductRepository productRepository;
@GetMapping
public List<Product> getAllProducts() {
return productRepository.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
return productRepository.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public Product createProduct(@Valid @RequestBody Product product) {
return productRepository.save(product);
}
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(
@PathVariable Long id,
@Valid @RequestBody Product productDetails) {
return productRepository.findById(id)
.map(product -> {
product.setName(productDetails.getName());
product.setDescription(productDetails.getDescription());
product.setPrice(productDetails.getPrice());
Product updatedProduct = productRepository.save(product);
return ResponseEntity.ok(updatedProduct);
})
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteProduct(@PathVariable Long id) {
return productRepository.findById(id)
.map(product -> {
productRepository.delete(product);
return ResponseEntity.ok().build();
})
.orElse(ResponseEntity.notFound().build());
}
}
@RestController: Combines @Controller and @ResponseBody@RequestMapping: Maps web requests to handler methods@GetMapping, @PostMapping, etc.: Shortcut for request method mapping@PathVariable: Binds a method parameter to a URI template variable@RequestBody: Binds HTTP request body to a method parameter@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(
ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
"NOT_FOUND",
ex.getMessage(),
HttpStatus.NOT_FOUND.value());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@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(
"VALIDATION_FAILED",
"Validation failed",
HttpStatus.BAD_REQUEST.value(),
errors);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
}
// ProductDTO.java
public class ProductDTO {
private String name;
private String description;
private double price;
// Constructors, getters and setters
public static ProductDTO fromEntity(Product product) {
ProductDTO dto = new ProductDTO();
dto.setName(product.getName());
dto.setDescription(product.getDescription());
dto.setPrice(product.getPrice());
return dto;
}
public Product toEntity() {
Product product = new Product();
product.setName(this.getName());
product.setDescription(this.getDescription());
product.setPrice(this.getPrice());
return product;
}
}
<!-- Add to pom.xml -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
// SwaggerConfig.java
@Configuration
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
return new ApiInfo(
"Product API",
"API for managing products",
"1.0",
"Terms of service",
new Contact("Your Name", "www.example.com", "contact@example.com"),
"License",
"License URL",
Collections.emptyList());
}
}
After adding Swagger, access the API documentation at:
http://localhost:8080/swagger-ui/ - Swagger UIhttp://localhost:8080/v2/api-docs - Raw API docs@ExtendWith(MockitoExtension.class)
class ProductControllerTest {
@Mock
private ProductRepository productRepository;
@InjectMocks
private ProductController productController;
@Test
void getProductById_shouldReturnProduct() {
// Arrange
Product product = new Product(1L, "Test", "Desc", 9.99);
when(productRepository.findById(1L)).thenReturn(Optional.of(product));
// Act
ResponseEntity<Product> response = productController.getProductById(1L);
// Assert
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals("Test", response.getBody().getName());
}
@Test
void getProductById_shouldReturnNotFound() {
when(productRepository.findById(1L)).thenReturn(Optional.empty());
ResponseEntity<Product> response = productController.getProductById(1L);
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
}
}
// GET request example
URL url = new URL("http://localhost:8080/api/products/1");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// Parse JSON response
ObjectMapper mapper = new ObjectMapper();
Product product = mapper.readValue(response.toString(), Product.class);
System.out.println(product.getName());
}
// GET request
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8080/api/products/1";
ResponseEntity<Product> response =
restTemplate.getForEntity(url, Product.class);
if (response.getStatusCode() == HttpStatus.OK) {
Product product = response.getBody();
System.out.println(product.getName());
}
// POST request
Product newProduct = new Product(null, "New", "Desc", 19.99);
ResponseEntity<Product> postResponse =
restTemplate.postForEntity(url, newProduct, Product.class);
WebClient webClient = WebClient.create("http://localhost:8080");
// GET request
Mono<Product> productMono = webClient.get()
.uri("/api/products/1")
.retrieve()
.bodyToMono(Product.class);
productMono.subscribe(product ->
System.out.println(product.getName()));
// POST request
Product newProduct = new Product(null, "New", "Desc", 19.99);
Mono<Product> createdProduct = webClient.post()
.uri("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(newProduct)
.retrieve()
.bodyToMono(Product.class);
My simple library
..of useful code