Реализовать REST API для поиска обуви с фильтрацией

4. Реализовать REST API для поиска обуви с фильтрацией

Условие задачи:
Необходимо реализовать REST API с эндпоинтом:

POST /api/v1/search

Эндпоинт принимает параметры фильтрации и возвращает подходящую обувь.

Условия поиска:

  • сапоги — цвет красный или зелёный, цена от 5000 до 20000;

  • туфли — цена меньше 10000.

Пример запроса:

{
  "type": "сапоги",
  "color": ["красный", "зелёный"],
  "minPrice": 5000,
  "maxPrice": 20000
}

Пример ответа:

[
  {
    "type": "сапоги",
    "color": "красный",
    "price": 15000,
    "url": "https://example.com/shoe1"
  }
]

Спойлеры к решению

Подсказки
💡 Создай сущность Shoe и DTO для запроса и ответа.
💡 Для динамических фильтров удобно использовать Specification.
💡 Репозиторий можно расширить через JpaSpecificationExecutor.
💡 Каждый переданный фильтр добавляй только тогда, когда он не равен null.
💡 Контроллер должен принимать JSON через @RequestBody.

Решение

Сущность:

@Entity
@Table(name = "shoes")
public class Shoe {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String type;
    private String color;
    private BigDecimal price;
    private String url;

    // getters / setters
}

DTO:

public record ShoeSearchRequest(
        String type,
        List<String> colors,
        BigDecimal minPrice,
        BigDecimal maxPrice
) {
}
public record ShoeResponse(
        String type,
        String color,
        BigDecimal price,
        String url
) {
}

Репозиторий:

public interface ShoeRepository
        extends JpaRepository<Shoe, Long>,
        JpaSpecificationExecutor<Shoe> {
}

Сервис:

@Service
public class ShoeService {

    private final ShoeRepository shoeRepository;

    public ShoeService(ShoeRepository shoeRepository) {
        this.shoeRepository = shoeRepository;
    }

    public List<ShoeResponse> search(
            ShoeSearchRequest request
    ) {
        Specification<Shoe> specification =
                Specification.where(null);

        if (request.type() != null) {
            specification = specification.and(
                    (root, query, cb) ->
                            cb.equal(root.get("type"), request.type())
            );
        }

        if (request.colors() != null
                && !request.colors().isEmpty()) {
            specification = specification.and(
                    (root, query, cb) ->
                            root.get("color").in(request.colors())
            );
        }

        if (request.minPrice() != null) {
            specification = specification.and(
                    (root, query, cb) ->
                            cb.greaterThanOrEqualTo(
                                    root.get("price"),
                                    request.minPrice()
                            )
            );
        }

        if (request.maxPrice() != null) {
            specification = specification.and(
                    (root, query, cb) ->
                            cb.lessThanOrEqualTo(
                                    root.get("price"),
                                    request.maxPrice()
                            )
            );
        }

        return shoeRepository.findAll(specification)
                .stream()
                .map(this::toResponse)
                .toList();
    }

    private ShoeResponse toResponse(Shoe shoe) {
        return new ShoeResponse(
                shoe.getType(),
                shoe.getColor(),
                shoe.getPrice(),
                shoe.getUrl()
        );
    }
}

Контроллер:

@RestController
@RequestMapping("/api/v1")
public class ShoeController {

    private final ShoeService shoeService;

    public ShoeController(ShoeService shoeService) {
        this.shoeService = shoeService;
    }

    @PostMapping("/search")
    public List<ShoeResponse> search(
            @RequestBody ShoeSearchRequest request
    ) {
        return shoeService.search(request);
    }
}

Specification позволяет динамически формировать запрос только из тех фильтров, которые действительно переданы клиентом.

Для денежных значений используется BigDecimal, а не Double, чтобы избежать ошибок представления чисел с плавающей точкой.