УРОК 9 / 30 0%

OpenAPI / Swagger

🎯

Цель урока

Сделать полноценный CRUD для задач с правильными HTTP-статусами (201/204/404), обработкой ошибок, и подготовить Task к подключению JPA.

🧠

Теория · для собеса

1 OpenAPI / Swagger — что это

OpenAPI Specification (OAS) — стандарт описания REST API (бывший Swagger).

Swagger UI — интерактивная HTML-страница, где можно:

  • посмотреть все endpoints
  • увидеть схемы запросов/ответов
  • попробовать вызвать прямо из браузера (try-it-out)

springdoc-openapi — Spring Boot интеграция, автогенерирует спецификацию из кода.

2 Что генерируется автоматически

springdoc сканирует:

  • @RestController классы → endpoints
  • @RequestMapping/@GetMapping/... → URL + HTTP-метод
  • @RequestBody/@PathVariable/@RequestParam → параметры
  • DTO-классы → JSON Schema (для request/response)
  • Javadoc на полях → описания
3 Что нужно добавлять вручную
  • Аннотации @Operation, @ApiResponse, @Schema для тонкой настройки
  • OpenAPI-метаданные (title, version, contact, license)

---

💻

Практика: улучшаем CRUD

  1. 1
    Зависимости
    
    dependencies {
        implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
    }
    
  2. 2
    OpenAPI-конфигурация
    
    package com.taskflow.config;
    
    import io.swagger.v3.oas.models.OpenAPI;
    import io.swagger.v3.oas.models.info.Contact;
    import io.swagger.v3.oas.models.info.Info;
    import io.swagger.v3.oas.models.info.License;
    import io.swagger.v3.oas.models.servers.Server;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.List;
    
    @Configuration
    public class OpenApiConfig {
    
        @Bean
        public OpenAPI taskflowOpenAPI() {
            return new OpenAPI()
                .info(new Info()
                    .title("TaskFlow API")
                    .description("Production-like Spring Boot Task Management Service")
                    .version("v0.1.0")
                    .contact(new Contact()
                        .name("Ivan")
                        .email("ivan@taskflow.local")
                        .url("https://taskflow.local"))
                    .license(new License()
                        .name("MIT")
                        .url("https://opensource.org/licenses/MIT")))
                .servers(List.of(
                    new Server().url("http://localhost:8080").description("Local dev"),
                    new Server().url("https://api.taskflow.local").description("Production")
                ));
        }
    }
    
  3. 3
    Аннотируем Controller
    
    @RestController
    @RequestMapping("/api/tasks")
    @Tag(name = "Tasks", description = "CRUD operations for tasks")
    public class TaskController {
    
        @Operation(
            summary = "Get all tasks",
            description = "Returns a list of all tasks, ordered by creation date (newest first)"
        )
        @ApiResponses({
            @ApiResponse(responseCode = "200", description = "Tasks found",
                content = @Content(mediaType = "application/json",
                    array = @ArraySchema(schema = @Schema(implementation = TaskResponse.class))))
        })
        @GetMapping
        public List findAll() {
            return service.findAll();
        }
    
        @Operation(summary = "Get task by id")
        @ApiResponses({
            @ApiResponse(responseCode = "200", description = "Task found",
                content = @Content(schema = @Schema(implementation = TaskResponse.class))),
            @ApiResponse(responseCode = "404", description = "Task not found",
                content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
        })
        @GetMapping("/{id}")
        public ResponseEntity findById(
            @Parameter(description = "Task ID", example = "1")
            @PathVariable Long id) {
            return service.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
        }
    
        @Operation(summary = "Create a new task")
        @ApiResponses({
            @ApiResponse(responseCode = "201", description = "Task created",
                content = @Content(schema = @Schema(implementation = TaskResponse.class))),
            @ApiResponse(responseCode = "400", description = "Validation failed",
                content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
        })
        @PostMapping
        public ResponseEntity create(
            @io.swagger.v3.oas.annotations.parameters.RequestBody(
                description = "Task to create",
                required = true,
                content = @Content(schema = @Schema(implementation = CreateTaskRequest.class)))
            @Valid @RequestBody CreateTaskRequest req) {
            TaskResponse created = service.create(req);
            URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
                .buildAndExpand(created.id()).toUri();
            return ResponseEntity.created(location).body(created);
        }
    
        @Operation(summary = "Update an existing task")
        @PutMapping("/{id}")
        public ResponseEntity update(
            @PathVariable Long id,
            @Valid @RequestBody UpdateTaskRequest req) {
            return service.update(id, req)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
        }
    
        @Operation(summary = "Delete a task")
        @DeleteMapping("/{id}")
        public ResponseEntity delete(@PathVariable Long id) {
            try { service.delete(id); return ResponseEntity.noContent().build(); }
            catch (ResourceNotFoundException e) { return ResponseEntity.notFound().build(); }
        }
    }
    
  4. 4
    Аннотируем DTO
    
    @Schema(description = "Request to create a new task")
    public record CreateTaskRequest(
        @Schema(description = "Task title", example = "Read Spring Boot docs", requiredMode = RequiredMode.REQUIRED)
        @NotBlank @Size(min = 3, max = 200)
        String title,
    
        @Schema(description = "Optional description", example = "Chapter 5-7")
        @Size(max = 5000)
        String description
    ) {}
    
  5. 5
    application.yml
    
    springdoc:
      api-docs:
        path: /v3/api-docs
      swagger-ui:
        path: /swagger-ui.html
        operations-sorter: method  # GET, POST, PUT, DELETE
        tags-sorter: alpha
      show-actuator: false
    
  6. 6
    Запуск
    
    ./gradlew bootRun
    

    Открываем:

    • Swagger UI: http://localhost:8080/swagger-ui.html
    • OpenAPI JSON: http://localhost:8080/v3/api-docs
    • OpenAPI YAML: http://localhost:8080/v3/api-docs.yaml

    ---

🎯

Зачем это на собесе

После урока ты должен уметь ответить на:

Клиентская команда (фронт, мобилка) может сама посмотреть контракт.
Try-it-out — QA может дёргать API прямо из браузера, не открывая Postman.
Генерация клиентов — из openapi.json генерируются TypeScript/Java/Python клиенты автоматически.
Контракт-тестирование — springdoc-сгенерированная схема сравнивается с эталоном в CI.

5 вопросов на углубление

Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.

1
Swagger 2.0 vs OpenAPI 3 — в чём разница?

Разбор внутри: старое название, новый стандарт и возможности схемы.

2
Как генерировать клиент из OpenAPI-контракта?

Разбор внутри: openapi.json, generator CLI и типизированный клиент.

3
Почему Optional в контроллере портит документацию?

Разбор внутри: Swagger не видит 404 без явного ResponseEntity.

4
@Tag vs GroupedOpenApi — как группировать API?

Разбор внутри: группировка по контроллерам и отдельные спецификации по путям.

5
Как правильно задавать examples в @Schema?

Разбор внутри: примеры полей, requiredMode и польза для клиента API.

Готов идти дальше?

Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.

🚀 Перейти к Уроку 10

Сначала пройди все секции и выбери хотя бы 1 вопрос

⬅️ Назад к Уроку 8
🎉
Новый тир
Новый тир достигнут!