Skip to content

Commit 433b309

Browse files
Difference between Swagger & HATEOAS (#17989)
* feat: swagger vs hateoas app and tests * fix: artifact and tests * fix: indent * clean: pom.xml * fix: properties * trigger build --------- Co-authored-by: luca <[email protected]>
1 parent 8598f60 commit 433b309

File tree

11 files changed

+369
-0
lines changed

11 files changed

+369
-0
lines changed

pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@
796796
<module>spring-batch-2</module>
797797
<module>spring-boot-modules</module>
798798
<module>spring-boot-rest</module>
799+
<module>spring-boot-rest-2</module>
799800
<module>spring-cloud-modules/spring-cloud-circuit-breaker</module>
800801
<module>spring-cloud-modules/spring-cloud-contract</module>
801802
<module>spring-cloud-modules/spring-cloud-eureka</module>

spring-boot-rest-2/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
## Spring Boot REST 2

spring-boot-rest-2/pom.xml

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<groupId>com.baeldung.web</groupId>
7+
<artifactId>spring-boot-rest-2</artifactId>
8+
<name>spring-boot-rest-2</name>
9+
<packaging>war</packaging>
10+
<description>Spring Boot Rest Module 2</description>
11+
12+
<parent>
13+
<groupId>com.baeldung</groupId>
14+
<artifactId>parent-boot-3</artifactId>
15+
<version>0.0.1-SNAPSHOT</version>
16+
<relativePath>../parent-boot-3</relativePath>
17+
</parent>
18+
19+
<dependencies>
20+
<dependency>
21+
<groupId>org.springframework.boot</groupId>
22+
<artifactId>spring-boot-starter-web</artifactId>
23+
</dependency>
24+
<!-- Spring HATEOAS -->
25+
<dependency>
26+
<groupId>org.springframework.boot</groupId>
27+
<artifactId>spring-boot-starter-hateoas</artifactId>
28+
</dependency>
29+
30+
<dependency>
31+
<groupId>org.springdoc</groupId>
32+
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
33+
<version>${springdoc-openapi.version}</version>
34+
</dependency>
35+
36+
<dependency>
37+
<groupId>org.springframework.boot</groupId>
38+
<artifactId>spring-boot-starter-test</artifactId>
39+
</dependency>
40+
</dependencies>
41+
42+
<build>
43+
<plugins>
44+
<plugin>
45+
<groupId>org.springframework.boot</groupId>
46+
<artifactId>spring-boot-maven-plugin</artifactId>
47+
</plugin>
48+
<plugin>
49+
<artifactId>maven-compiler-plugin</artifactId>
50+
<configuration>
51+
<compilerArgs>
52+
<arg>-parameters</arg>
53+
</compilerArgs>
54+
<source>17</source>
55+
<target>17</target>
56+
</configuration>
57+
</plugin>
58+
</plugins>
59+
</build>
60+
61+
<properties>
62+
<springdoc-openapi.version>2.5.0</springdoc-openapi.version>
63+
</properties>
64+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.baeldung;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class SpringBootRestApplication {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(SpringBootRestApplication.class, args);
11+
}
12+
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.baeldung.hateoasvsswagger;
2+
3+
import com.baeldung.hateoasvsswagger.model.NewUser;
4+
import com.baeldung.hateoasvsswagger.model.User;
5+
import com.baeldung.hateoasvsswagger.repository.UserRepository;
6+
7+
import io.swagger.v3.oas.annotations.Operation;
8+
import io.swagger.v3.oas.annotations.media.Content;
9+
import io.swagger.v3.oas.annotations.media.Schema;
10+
import io.swagger.v3.oas.annotations.parameters.RequestBody;
11+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
12+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
13+
14+
import org.springframework.http.HttpStatus;
15+
import org.springframework.http.MediaType;
16+
import org.springframework.http.ResponseEntity;
17+
import org.springframework.web.bind.annotation.GetMapping;
18+
import org.springframework.web.bind.annotation.PathVariable;
19+
import org.springframework.web.bind.annotation.PostMapping;
20+
import org.springframework.web.bind.annotation.RequestMapping;
21+
import org.springframework.web.bind.annotation.RestController;
22+
23+
import java.util.List;
24+
25+
@RestController
26+
@RequestMapping("/api/users")
27+
public class UserController {
28+
29+
private final UserRepository userRepository = new UserRepository();
30+
31+
@Operation(summary = "Get all users", description = "Retrieve a list of all users")
32+
@ApiResponses(value = {
33+
@ApiResponse(responseCode = "200", description = "List of users", content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
34+
@ApiResponse(responseCode = "500", description = "Internal server error") })
35+
@GetMapping
36+
public ResponseEntity<List<User>> getAllUsers() {
37+
return ResponseEntity.ok()
38+
.body(userRepository.getAllUsers());
39+
}
40+
41+
@Operation(summary = "Create a new user", description = "Add a new user to the system")
42+
@ApiResponses(value = {
43+
@ApiResponse(responseCode = "201", description = "User created", content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
44+
@ApiResponse(responseCode = "400", description = "Invalid input") })
45+
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
46+
public ResponseEntity<User> createUser(
47+
@RequestBody(description = "User data", required = true, content = @Content(schema = @Schema(implementation = NewUser.class))) NewUser user) {
48+
return new ResponseEntity<>(userRepository.createUser(user), HttpStatus.CREATED);
49+
}
50+
51+
@Operation(summary = "Get user by ID", description = "Retrieve a user by their unique ID")
52+
@ApiResponses(value = {
53+
@ApiResponse(responseCode = "200", description = "User found", content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
54+
@ApiResponse(responseCode = "404", description = "User not found") })
55+
@GetMapping("/{id}")
56+
public ResponseEntity<User> getUserById(@PathVariable Integer id) {
57+
return ResponseEntity.ok()
58+
.body(userRepository.getUserById(id));
59+
}
60+
}
61+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.baeldung.hateoasvsswagger;
2+
3+
import com.baeldung.hateoasvsswagger.model.NewUser;
4+
import com.baeldung.hateoasvsswagger.model.User;
5+
import com.baeldung.hateoasvsswagger.repository.UserRepository;
6+
7+
import org.springframework.hateoas.CollectionModel;
8+
import org.springframework.hateoas.EntityModel;
9+
import org.springframework.http.MediaType;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.annotation.GetMapping;
12+
import org.springframework.web.bind.annotation.PathVariable;
13+
import org.springframework.web.bind.annotation.PostMapping;
14+
import org.springframework.web.bind.annotation.RequestBody;
15+
import org.springframework.web.bind.annotation.RequestMapping;
16+
import org.springframework.web.bind.annotation.RestController;
17+
18+
import java.util.List;
19+
20+
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
21+
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
22+
23+
@RestController
24+
@RequestMapping("/api/hateoas/users")
25+
public class UserHateoasController {
26+
27+
private final UserRepository userService; // Assume a service layer handles business logic
28+
29+
public UserHateoasController(UserRepository userService) {
30+
this.userService = userService;
31+
}
32+
33+
@GetMapping
34+
public CollectionModel<User> getAllUsers() {
35+
List<User> users = userService.getAllUsers();
36+
37+
users.forEach(user -> {
38+
user.add(linkTo(methodOn(UserController.class).getUserById(user.getId())).withSelfRel());
39+
});
40+
41+
return CollectionModel.of(users, linkTo(methodOn(UserController.class).getAllUsers()).withSelfRel());
42+
}
43+
44+
@GetMapping("/{id}")
45+
public EntityModel<User> getUserById(@PathVariable Integer id) {
46+
User user = userService.getUserById(id);
47+
user.add(linkTo(methodOn(UserController.class).getUserById(id)).withSelfRel());
48+
user.add(linkTo(methodOn(UserController.class).getAllUsers()).withRel("all-users"));
49+
return EntityModel.of(user);
50+
}
51+
52+
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
53+
public ResponseEntity<EntityModel<User>> createUser(@RequestBody NewUser user) {
54+
User createdUser = userService.createUser(user);
55+
createdUser.add(linkTo(methodOn(UserController.class).getUserById(createdUser.getId())).withSelfRel());
56+
return ResponseEntity.created(linkTo(methodOn(UserController.class).getUserById(createdUser.getId())).toUri())
57+
.body(EntityModel.of(createdUser));
58+
}
59+
}
60+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.baeldung.hateoasvsswagger.model;
2+
3+
public record NewUser(String name, String email) {
4+
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.baeldung.hateoasvsswagger.model;
2+
3+
import org.springframework.hateoas.RepresentationModel;
4+
5+
import java.time.LocalDateTime;
6+
7+
public class User extends RepresentationModel<User> {
8+
9+
private Integer id;
10+
private String name;
11+
private String email;
12+
private LocalDateTime createdAt;
13+
14+
public Integer getId() {
15+
return id;
16+
}
17+
18+
public void setId(Integer id) {
19+
this.id = id;
20+
}
21+
22+
public String getName() {
23+
return name;
24+
}
25+
26+
public void setName(String name) {
27+
this.name = name;
28+
}
29+
30+
public String getEmail() {
31+
return email;
32+
}
33+
34+
public void setEmail(String email) {
35+
this.email = email;
36+
}
37+
38+
public LocalDateTime getCreatedAt() {
39+
return createdAt;
40+
}
41+
42+
public void setCreatedAt(LocalDateTime createdAt) {
43+
this.createdAt = createdAt;
44+
}
45+
46+
public User(int id, String name, String email, LocalDateTime createdAt) {
47+
this.id = id;
48+
this.name = name;
49+
this.email = email;
50+
this.createdAt = createdAt;
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.baeldung.hateoasvsswagger.repository;
2+
3+
import com.baeldung.hateoasvsswagger.model.NewUser;
4+
import com.baeldung.hateoasvsswagger.model.User;
5+
import org.springframework.stereotype.Repository;
6+
7+
import java.time.LocalDateTime;
8+
import java.util.HashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
@Repository
13+
public class UserRepository {
14+
15+
private final Map<Integer, User> users = new HashMap<>();
16+
17+
public UserRepository() {
18+
users.put(1, new User(1, "Baeldung", "[email protected]", LocalDateTime.now()));
19+
}
20+
21+
public List<User> getAllUsers() {
22+
return users.values().stream().map(u -> new User(u.getId(), u.getName(), u.getEmail(), LocalDateTime.now())).toList();
23+
}
24+
25+
public User getUserById(int id) {
26+
return users.get(id);
27+
}
28+
29+
public User createUser(NewUser user) {
30+
int id = users.size() + 1;
31+
return users.put(id, new User(id, user.name(), user.email(), LocalDateTime.now()));
32+
}
33+
}

spring-boot-rest-2/src/main/resources/application.properties

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.baeldung.hateoasvsswagger;
2+
3+
import com.baeldung.hateoasvsswagger.model.NewUser;
4+
import com.baeldung.hateoasvsswagger.model.User;
5+
import com.baeldung.hateoasvsswagger.repository.UserRepository;
6+
7+
import org.junit.jupiter.api.Test;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
10+
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
11+
import org.springframework.boot.test.mock.mockito.MockBean;
12+
import org.springframework.http.MediaType;
13+
import org.springframework.test.web.servlet.MockMvc;
14+
15+
import java.time.LocalDateTime;
16+
import java.util.List;
17+
18+
import static org.mockito.ArgumentMatchers.any;
19+
import static org.mockito.Mockito.when;
20+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
21+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
22+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
23+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
24+
25+
@WebMvcTest(UserHateoasController.class)
26+
@AutoConfigureMockMvc
27+
class HateoasControllerIntegrationTest {
28+
29+
@Autowired
30+
private MockMvc mockMvc;
31+
32+
@MockBean
33+
private UserRepository userService;
34+
35+
@Test
36+
void whenAllUsersRequested_thenReturnAllUsersWithLinks() throws Exception {
37+
User user1 = new User(1, "John Doe", "[email protected]", LocalDateTime.now());
38+
User user2 = new User(2, "Jane Smith", "[email protected]", LocalDateTime.now());
39+
40+
when(userService.getAllUsers()).thenReturn(List.of(user1, user2));
41+
42+
mockMvc.perform(get("/api/hateoas/users").accept(MediaType.APPLICATION_JSON))
43+
.andExpect(status().isOk())
44+
.andExpect(jsonPath("$._embedded.userList[0].id").value(1))
45+
.andExpect(jsonPath("$._embedded.userList[0].name").value("John Doe"))
46+
.andExpect(jsonPath("$._embedded.userList[0]._links.self.href").exists())
47+
.andExpect(jsonPath("$._embedded.userList[1].id").value(2))
48+
.andExpect(jsonPath("$._embedded.userList[1].name").value("Jane Smith"))
49+
.andExpect(jsonPath("$._links.self.href").exists());
50+
}
51+
52+
@Test
53+
void whenUserByIdRequested_thenReturnUserByIdWithLinks() throws Exception {
54+
User user = new User(1, "John Doe", "[email protected]", LocalDateTime.now());
55+
56+
when(userService.getUserById(1)).thenReturn(user);
57+
58+
mockMvc.perform(get("/api/hateoas/users/1").accept(MediaType.APPLICATION_JSON))
59+
.andExpect(status().isOk())
60+
.andExpect(jsonPath("$.id").value(1))
61+
.andExpect(jsonPath("$.name").value("John Doe"))
62+
.andExpect(jsonPath("$.email").value("[email protected]"))
63+
.andExpect(jsonPath("$._links.self.href").exists())
64+
.andExpect(jsonPath("$._links.all-users.href").exists());
65+
}
66+
67+
@Test
68+
void whenUserCreationRequested_thenReturnUserByIdWithLinks() throws Exception {
69+
User user = new User(1, "John Doe", "[email protected]", LocalDateTime.now());
70+
when(userService.createUser(any(NewUser.class))).thenReturn(user);
71+
72+
mockMvc.perform(post("/api/hateoas/users").contentType(MediaType.APPLICATION_JSON)
73+
.content("{\"name\":\"John Doe\",\"email\":\"[email protected]\"}"))
74+
.andExpect(status().isCreated())
75+
.andExpect(jsonPath("$.id").value(1))
76+
.andExpect(jsonPath("$.name").value("John Doe"))
77+
.andExpect(jsonPath("$._links.self.href").exists());
78+
}
79+
}

0 commit comments

Comments
 (0)