Skip to content

Commit 2901e6c

Browse files
Integrated Swagger OpenAPI docs and add HATEOAS links
1 parent 010b116 commit 2901e6c

File tree

12 files changed

+262
-189
lines changed

12 files changed

+262
-189
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,32 @@
1515
- Spring Web (REST API)
1616
- Spring Data JPA (встроенный Hibernate)
1717
- PostgreSQL (через Docker Compose)
18+
- Spring HATEOAS (hypermedia links в API)
19+
- Swagger/OpenAPI (автоматическая документация)
1820
- SLF4J + Logback (логирование)
1921
- JUnit 5 + MockMvc (тестирование контроллеров и API)
2022
- Maven (сборка и зависимости)
2123
- Checkstyle (проверка стиля кода)
2224
- Lombok — (автогенерации геттеров/сеттеров)
2325
---
2426

27+
## Описание API и документация
28+
29+
### Swagger OpenAPI
30+
31+
В проект интегрирована автоматическая генерация документации с помощью **Swagger (OpenAPI)**.
32+
Документация доступна после запуска приложения:
33+
34+
- **Swagger UI:** [http://localhost:8080/swagger-ui/index.html](http://localhost:8080/swagger-ui/index.html)
35+
- **OpenAPI JSON:** [http://localhost:8080/v3/api-docs](http://localhost:8080/v3/api-docs)
36+
37+
### Поддержка HATEOAS
38+
39+
Сервис реализует подход **HATEOAS** (Hypermedia as the Engine of Application State):
40+
41+
- Все ответы API содержат _hypermedia-ссылки_, помогающие навигировать по ресурсам (например, ссылки на просмотр, редактирование и удаление пользователя).
42+
- Это упрощает клиентскую интеграцию и соответствует лучшим REST-практикам.
43+
2544
## Запуск проекта
2645

2746
### 1. Клонировать репозиторий
@@ -61,3 +80,5 @@ java -jar target/user-service-spring.jar
6180

6281
### 7. Пример работы приложения
6382
![img_1.png](readme-resources/img_1.png)
83+
![img_2.png](readme-resources/img_2.png)
84+
![img_3.png](readme-resources/img_3.png)

pom.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,23 @@
2929
<groupId>org.springframework.boot</groupId>
3030
<artifactId>spring-boot-starter-validation</artifactId>
3131
</dependency>
32+
<dependency>
33+
<groupId>org.springframework.boot</groupId>
34+
<artifactId>spring-boot-starter-hateoas</artifactId>
35+
</dependency>
36+
<dependency>
37+
<groupId>org.springdoc</groupId>
38+
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
39+
<version>2.8.9</version>
40+
</dependency>
3241
<dependency>
3342
<groupId>org.springframework.boot</groupId>
3443
<artifactId>spring-boot-starter-data-jpa</artifactId>
3544
</dependency>
45+
<dependency>
46+
<groupId>org.springframework.boot</groupId>
47+
<artifactId>spring-boot-starter-actuator</artifactId>
48+
</dependency>
3649
<dependency>
3750
<groupId>org.postgresql</groupId>
3851
<artifactId>postgresql</artifactId>
@@ -43,6 +56,12 @@
4356
<artifactId>lombok</artifactId>
4457
<scope>provided</scope>
4558
</dependency>
59+
<dependency>
60+
<groupId>org.mockito</groupId>
61+
<artifactId>mockito-junit-jupiter</artifactId>
62+
<version>5.2.0</version>
63+
<scope>test</scope>
64+
</dependency>
4665
<dependency>
4766
<groupId>org.springframework.boot</groupId>
4867
<artifactId>spring-boot-starter-test</artifactId>
@@ -60,6 +79,7 @@
6079
<version>1.19.7</version>
6180
<scope>test</scope>
6281
</dependency>
82+
6383
</dependencies>
6484
<build>
6585
<plugins>

readme-resources/img_2.png

46.2 KB
Loading

readme-resources/img_3.png

29 KB
Loading

src/main/java/myuserservice/advice/GlobalExceptionHandler.java

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,40 @@
88
import org.springframework.web.bind.annotation.ExceptionHandler;
99
import org.springframework.web.bind.annotation.RestControllerAdvice;
1010

11+
import java.util.HashMap;
12+
import java.util.Map;
1113
import java.util.stream.Collectors;
1214

13-
1415
@RestControllerAdvice
1516
public class GlobalExceptionHandler {
1617

1718
@ExceptionHandler(EntityNotFoundException.class)
18-
public ResponseEntity<String> handleNotFound(EntityNotFoundException ex) {
19-
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
19+
public ResponseEntity<Map<String, Object>> handleNotFound(EntityNotFoundException ex) {
20+
Map<String, Object> body = new HashMap<>();
21+
body.put("error", "Not Found");
22+
body.put("message", ex.getMessage());
23+
body.put("status", HttpStatus.NOT_FOUND.value());
24+
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
2025
}
2126

2227
@ExceptionHandler(DataIntegrityViolationException.class)
23-
public ResponseEntity<String> handleDuplicate(DataIntegrityViolationException ex) {
24-
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Email already exists");
28+
public ResponseEntity<Map<String, Object>> handleDuplicate(DataIntegrityViolationException ex) {
29+
Map<String, Object> body = new HashMap<>();
30+
body.put("error", "Bad Request");
31+
body.put("message", "Email already exists");
32+
body.put("status", HttpStatus.BAD_REQUEST.value());
33+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
2534
}
2635

2736
@ExceptionHandler(MethodArgumentNotValidException.class)
28-
public ResponseEntity<String> handleValidation(MethodArgumentNotValidException ex) {
37+
public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
2938
String message = ex.getBindingResult().getFieldErrors().stream()
3039
.map(err -> err.getField() + ": " + err.getDefaultMessage())
3140
.collect(Collectors.joining(", "));
32-
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Validation error: " + message);
33-
}
34-
35-
@ExceptionHandler(Exception.class)
36-
public ResponseEntity<String> handleGeneric(Exception ex) {
37-
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
38-
.body("Unexpected error occurred");
41+
Map<String, Object> body = new HashMap<>();
42+
body.put("error", "Validation Error");
43+
body.put("message", message);
44+
body.put("status", HttpStatus.BAD_REQUEST.value());
45+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
3946
}
40-
}
47+
}

src/main/java/myuserservice/controller/UserController.java

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33

44
import myuserservice.dto.UserDto;
55
import myuserservice.service.UserService;
6+
import myuserservice.hateoas.UserModelAssembler;
67
import org.slf4j.Logger;
78
import org.slf4j.LoggerFactory;
9+
import org.springframework.hateoas.CollectionModel;
10+
import org.springframework.hateoas.EntityModel;
11+
import org.springframework.http.ResponseEntity;
812
import org.springframework.web.bind.annotation.RestController;
913
import org.springframework.web.bind.annotation.RequestMapping;
1014
import org.springframework.web.bind.annotation.PostMapping;
@@ -23,48 +27,61 @@ public class UserController {
2327

2428
private static final Logger log = LoggerFactory.getLogger(UserController.class);
2529
private final UserService userService;
30+
private final UserModelAssembler assembler;
2631

27-
public UserController(UserService userService) {
32+
public UserController(UserService userService, UserModelAssembler assembler) {
2833
this.userService = userService;
34+
this.assembler = assembler;
2935
}
3036

3137
@PostMapping
32-
public UserDto createUser (@Valid @RequestBody UserDto userDto) {
38+
public EntityModel<UserDto> createUser (@Valid @RequestBody UserDto userDto) {
3339
log.info("POST /api/users — Запрос на создание пользователя: {}", userDto);
3440
UserDto created = userService.createUser(userDto);
3541
log.info("Пользователь создан: id={}, email={}", created.getId(), created.getEmail());
36-
return created;
42+
return assembler.toModel(created) ;
3743
}
3844

3945
@GetMapping("/{id}")
40-
public UserDto getUserById(@PathVariable Long id) {
46+
public EntityModel<UserDto> getUserById(@PathVariable Long id) {
4147
log.info("GET /api/users/{} — Запрос пользователя по id", id);
4248
UserDto user = userService.getUser(id);
4349
log.info("Пользователь найден: id={}, email={}", user.getId(), user.getEmail());
44-
return user;
50+
return assembler.toModel(user);
4551
}
4652

4753
@GetMapping
48-
public List<UserDto> getAllUsers() {
54+
public CollectionModel<EntityModel<UserDto>> getAllUsers() {
4955
log.info("GET /api/users — Запрос списка всех пользователей");
5056
List<UserDto> users = userService.getAllUsers();
5157
log.info("Найдено пользователей: {}", users.size());
52-
return users;
58+
59+
List<EntityModel<UserDto>> userModels = users.stream()
60+
.map(assembler::toModel)
61+
.toList();
62+
return CollectionModel.of(
63+
userModels,
64+
org.springframework.hateoas.server.mvc.WebMvcLinkBuilder
65+
.linkTo(org.springframework.hateoas.server.mvc.WebMvcLinkBuilder
66+
.methodOn(UserController.class).getAllUsers())
67+
.withSelfRel()
68+
);
5369
}
5470

5571
@PutMapping("/{id}")
56-
public UserDto updateUser(@PathVariable Long id, @RequestBody UserDto userDto) {
72+
public EntityModel<UserDto > updateUser(@PathVariable Long id, @RequestBody UserDto userDto) {
5773
log.info("PUT /api/users/{} — Запрос на обновление пользователя: {}", id, userDto);
5874
UserDto updated = userService.updateUser(id, userDto);
5975
log.info("Пользователь обновлён: {}", updated);
60-
return updated;
76+
return assembler.toModel(updated) ;
6177
}
6278

6379
@DeleteMapping("/{id}")
64-
public void deleteUser(@PathVariable Long id) {
80+
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
6581
log.info("DELETE /api/users/{} — Запрос на удаление пользователя", id);
6682
userService.deleteUser(id);
6783
log.info("Пользователь с id={} удалён", id);
84+
return ResponseEntity.noContent().build();
6885
}
6986
}
7087

src/main/java/myuserservice/dto/UserDto.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package myuserservice.dto;
22

3-
4-
53
import com.fasterxml.jackson.annotation.JsonProperty;
64
import lombok.AllArgsConstructor;
75
import lombok.Builder;
@@ -10,12 +8,14 @@
108
import jakarta.validation.constraints.NotNull;
119
import jakarta.validation.constraints.Size;
1210
import jakarta.validation.constraints.Email;
11+
import org.springframework.hateoas.server.core.Relation;
1312

1413
@Data
1514
@NoArgsConstructor
1615
@AllArgsConstructor
1716
@Builder
1817

18+
@Relation(collectionRelation = "users")
1919
public class UserDto {
2020

2121
private Long id;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package myuserservice.hateoas;
2+
3+
import myuserservice.dto.UserDto;
4+
import myuserservice.controller.UserController;
5+
import org.springframework.hateoas.EntityModel;
6+
import org.springframework.hateoas.server.RepresentationModelAssembler;
7+
import org.springframework.stereotype.Component;
8+
9+
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
10+
11+
@Component
12+
public class UserModelAssembler implements RepresentationModelAssembler<UserDto, EntityModel<UserDto>> {
13+
14+
@Override
15+
public EntityModel<UserDto> toModel(UserDto user) {
16+
return EntityModel.of(user,
17+
linkTo(methodOn(UserController.class).getUserById(user.getId())).withSelfRel(),
18+
linkTo(methodOn(UserController.class).deleteUser(user.getId())).withRel("delete"),
19+
linkTo(methodOn(UserController.class).updateUser(user.getId(), user)).withRel("update"),
20+
linkTo(methodOn(UserController.class).getAllUsers()).withRel("all-users"));
21+
}
22+
23+
}

src/main/resources/application.properties

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,3 @@ logging.level.org.hibernate.type.descriptor.sql=TRACE
99
spring.http.encoding.charset=UTF-8
1010
spring.http.encoding.enabled=true
1111
spring.http.encoding.force=true
12-

0 commit comments

Comments
 (0)