middle
Как проектировать REST API в Spring Boot?
Проектирование REST API в 2026 году строится по принципу API-first: сначала контракт (OpenAPI 3.1), потом реализация.
OpenAPI 3.1 спецификация
Пример OpenAPI-спецификации
openapi: 3.1.0
info:
title: Order Service API
version: 1.0.0
paths:
/orders:
post:
operationId: createOrder
summary: Создать новый заказ
tags: [Orders]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrderRequest'
responses:
'201':
description: Заказ создан
headers:
Location:
schema:
type: string
format: uri
'400':
$ref: '#/components/responses/BadRequest'
/orders/{orderId}:
get:
operationId: getOrder
parameters:
- name: orderId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Успешный ответ
'404':
$ref: '#/components/responses/NotFound'
Контроллер с версионированием API
Версионирование через URL-путь (/api/v1/orders) — наиболее простой и распространённый подход:
Пример
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final CreateOrderUseCase createOrderUseCase;
private final OrderDtoMapper mapper;
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
Order order = createOrderUseCase.create(mapper.toDomain(request));
URI location = URI.create("/api/v1/orders/" + order.getId());
return ResponseEntity.created(location).body(mapper.toResponse(order));
}
@GetMapping("/{orderId}")
public OrderResponse getOrder(@PathVariable UUID orderId) {
return mapper.toResponse(getOrderUseCase.getById(orderId));
}
}
Problem Detail (RFC 9457) для ошибок
Spring Boot 3.x нативно поддерживает стандарт Problem Detail:
GlobalExceptionHandler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ProblemDetail handleOrderNotFound(OrderNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND, ex.getMessage());
problem.setTitle("Заказ не найден");
problem.setType(URI.create("https://api.example.com/errors/order-not-found"));
problem.setProperty("orderId", ex.getOrderId());
return problem;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, "Ошибка валидации");
Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "invalid",
(a, b) -> a));
problem.setProperty("fieldErrors", errors);
return problem;
}
}
REST vs gRPC vs GraphQL
| Критерий | REST | gRPC | GraphQL |
|---|---|---|---|
| Формат | JSON | Protobuf (бинарный) | JSON |
| Контракт | OpenAPI 3.1 | .proto файлы | Schema |
| Производительность | Средняя | Высокая | Средняя |
| Подходит для | Public API, CRUD | Inter-service, высоконагруженные | Гибкие клиентские запросы |
| Streaming | Нет (SSE/WS) | Да (native) | Subscriptions |
Частые ошибки
- Возврат доменных сущностей напрямую из API вместо DTO — утечка внутренней структуры
- Отсутствие версионирования API — любое breaking change ломает клиентов
- Использование HTTP 200 для всех ответов с кодом ошибки в теле — нарушение семантики HTTP
- Отсутствие пагинации для коллекций
На собеседовании: интервьюер часто спрашивает про обработку ошибок в API. Упомяните Problem Detail (RFC 9457) и покажите, что знаете разницу между 400 (валидация) и 422 (бизнес-ошибка). Вопрос “REST или gRPC?” — ответ зависит от контекста: REST для public API, gRPC для inter-service.