JavaRush /Курсы /Spring Test /API: ошибки и пагинация

API: ошибки и пагинация

Spring Test
26 уровень , 1 лекция
Открыта

1. Public API и цель документации

Если подойти к REST Docs с энтузиазмом «а давайте задокументируем вообще всё, что возвращает сервер», вы очень быстро превратите документацию в музей случайностей. Вчера вы добавили поле debugInfo «на минутку», сегодня забыли убрать, завтра клиент радостно начал на него полагаться, а потом вы его удалили — и у вас новый сезон сериала “Breaking Changes: The Reckoning”.

Поэтому в ContentHub мы начинаем с public surface: то, что реально потребляют внешние клиенты (или хотя бы другие части вашей системы). Это прежде всего GET /api/public/articles и GET /api/public/articles/{slug}, плюс обязательные спутники любого публичного API: пагинация и ошибки. Внутренние детали сервиса, «как мы там внутри выбираем статьи из базы», REST Docs не должны пересказывать. Документация — это контракт на границе HTTP, а не автобиография вашего @Service.

Чтобы не сбиться в «документируем всё подряд», полезно держать в голове простую (и очень не романтическую) табличку приоритетов:

Что документируем Почему это важно клиенту Типичные последствия, если не документировать
Public list/details endpoints Это главный вход в систему Клиент пишет код «по догадке» и начинает угадывать контракт
Query-параметры пагинации/сортировки/фильтров Клиенту нужно знать формат и дефолты Бесконечные “а почему page начинается с 0?”, “а sort как?”
Page wrapper (метаданные страницы) Клиент строит UI пагинации UI ломается, потому что totalPages внезапно исчез
Error contract (ApiProblem) Ошибки — тоже часть API Клиент не умеет различать 404 и 409, не показывает нарушения

Идея лекции: мы документируем экономно, но строго по смыслу. То есть мы описываем важные поля, параметры и структуры, а не делаем полный снимок JSON «как бог пошлёт сериализацию сегодня».

2. Query-параметры GET /api/public/articles

Пагинация и фильтры часто воспринимаются как «ну это же мелочи, клиент сам догадается». А потом вы видите в чате команды: “Ребята, а page у нас с нуля или с единицы?”, “А size ограничен?”, “А сортировка через запятую или через пробел?”. И вот вы уже обсуждаете API не по документации, а по памяти людей. Память людей — штука творческая, поэтому лучше ей не доверять.

REST Docs тут хорош тем, что он заставляет нас честно описать входной контракт: какие query params есть, что они значат, какие из них необязательные, какие у них значения по умолчанию. И главное — мы описываем это в тесте, который уже проверяет, что endpoint отвечает 200 OK и возвращает JSON правильной формы.

Скелет теста для REST Docs обычно выглядит так (мы остаёмся в MVC slice, потому что документируем HTTP-границу, а не базу данных):

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.assertj.MockMvcTester;

// MVC-slice тест: поднимаем только веб-слой, чтобы проверять HTTP-контракт
@WebMvcTest(PublicArticleController.class)
@AutoConfigureRestDocs // Включаем генерацию snippets в build/generated-snippets
class PublicArticleRestDocsTest {

    @Autowired
    MockMvcTester mvc; // Упрощённый клиент для выполнения HTTP-запросов к MockMvc
}

Теперь добавим в тест именно документацию query parameters. Обратите внимание на стиль: сначала мы проверяем статус, и только потом «навешиваем» document(...). Это важно психологически: документация — не главная цель теста, она вторична по отношению к проверке контракта.

import org.junit.jupiter.api.Test; 

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;

@Test
void shouldDocumentPublicArticlesQueryParams() {
    // Делаем реальный (для теста) HTTP-вызов к контроллеру
    assertThat(mvc.get().uri("/api/public/articles?page=0&size=20&sort=publishedAt,desc"))
        // 1) Сначала проверяем, что happy path действительно happy
        .hasStatusOk()
        // 2) Только после этого генерируем сниппеты документации
        .apply(document("public-articles-list",
            queryParameters(
                // Описываем входной контракт: какие параметры принимает endpoint
                parameterWithName("page").optional().description("Номер страницы (0-based)"),
                parameterWithName("size").optional().description("Размер страницы"),
                parameterWithName("sort").optional().description("Сортировка в формате field,(asc|desc)")
            )
        ));
}

Пара важных нюансов, которые обычно всплывают не сразу, а после первой «битвы с реальностью».

Во‑первых, optional() здесь — не просто “ну оно как бы необязательное”. Это ещё и про то, что вы, возможно, запускаете тест без этого параметра (например, дефолтный page=0, size=20). Если вы забудете optional(), REST Docs будет ожидать параметр всегда и начнёт ругаться, когда вы вызываете endpoint без sort. Документация должна отражать реальный контракт: параметр необязателен — значит необязателен.

Во‑вторых, описания в description(...) лучше писать как для клиента, который впервые видит API. Фраза “page param” — это почти как написать в коде int x; // x. Формально комментарий есть, практической пользы — ноль.

Чтобы сделать документацию нагляднее, часто удобно держать у себя в голове «внутреннюю таблицу» для параметров пагинации (в REST Docs она напрямую не вставляется, но помогает правильно формулировать дескрипторы):

Параметр Пример Что означает Вопрос, который снимает
page 0 номер страницы “нумерация с 0 или с 1?”
size 20 размер страницы “какой дефолт? есть ли лимит?”
sort publishedAt,desc сортировка “как задавать направление сортировки?”

Если у GET /api/public/articles есть фильтры, например category, его так же надо задокументировать — иначе клиент будет искать «магический параметр» методом археологии:

import org.junit.jupiter.api.Test; 

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;

@Test
void shouldDocumentCategoryFilter() {
    // Сценарий: клиент фильтрует список статей по категории
    assertThat(mvc.get().uri("/api/public/articles?category=java&page=0&size=20"))
        .hasStatusOk() // Важно: фильтр не должен "ломать" успешный ответ
        .apply(document("public-articles-list-with-category",
            queryParameters(
                // Фильтры — тоже часть публичного контракта
                parameterWithName("category").optional().description("Фильтр по коду категории"),
                // Пагинация остаётся такой же
                parameterWithName("page").optional().description("Номер страницы (0-based)"),
                parameterWithName("size").optional().description("Размер страницы")
            )
        ));
}

Да, snippets будут множиться. И это нормально, пока они не становятся “по одному на каждый чих”. Смысл не в количестве файлов, а в том, чтобы каждый сниппет закрывал отдельный стабильный сценарий.

3. Pagination-ответ: wrapper и content[]

Пагинированный ответ — это место, где “вроде всё понятно” превращается в “почему на клиенте невозможно нормально сделать пагинацию”. Клиенту обычно нужны две вещи: список элементов (у нас это статьи) и метаданные страницы (где мы находимся, сколько всего страниц, какой размер). Если метаданные не документировать, каждый клиент начинает интерпретировать ваш API по‑своему.

В ContentHub мы не возвращаем наружу Page<Article> напрямую (и это правильно), а используем wrapper вроде PageResponse. Поэтому REST Docs в идеале должен отдельно показать структуру wrapper’а и структуру элемента списка. И тут важно не смешивать всё в одну кашу из fieldWithPath("content[].something") без понимания.

Начнём с базового документационного описания PageResponse, не углубляясь в поля элемента. Это как подписать коробку “тут лежит страница результатов” и указать, что внутри есть content, page, size, totalElements, totalPages.

import org.junit.jupiter.api.Test; 

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;

@Test
void shouldDocumentPublicArticlesPageWrapper() {
    assertThat(mvc.get().uri("/api/public/articles?page=0&size=20"))
        .hasStatusOk() // Проверяем, что пагинация в принципе работает
        .apply(document("public-articles-list-page",
            responseFields(
                // Описываем wrapper страницы (метаданные + список)
                fieldWithPath("content").description("Список статей на странице"),
                fieldWithPath("page").description("Номер страницы (0-based)"),
                fieldWithPath("size").description("Размер страницы"),
                fieldWithPath("totalElements").description("Общее число найденных статей"),
                fieldWithPath("totalPages").description("Общее число страниц")
            )
        ));
}

Теперь документируем элементы content[]. И вот здесь многие начинают делать «полный снимок карточки статьи» со всеми полями, включая те, которые клиенту вообще не нужны. В public list обычно достаточно того, что реально отображается в списке: slug, title, summary, publishedAt, что-нибудь про категорию.

import org.junit.jupiter.api.Test; 

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;

@Test
void shouldDocumentArticleCardInList() {
    assertThat(mvc.get().uri("/api/public/articles?page=0&size=20"))
        .hasStatusOk()
        .apply(document("public-articles-list-content",
            responseFields(
                // Поля, которые клиент обычно показывает в списке
                fieldWithPath("content[].slug").description("Публичный идентификатор статьи"),
                fieldWithPath("content[].title").description("Заголовок статьи"),
                fieldWithPath("content[].summary").description("Короткое описание"),
                fieldWithPath("content[].publishedAt").description("Время публикации"),
                // Минимум метаданных страницы, чтобы UI мог строить пагинацию
                fieldWithPath("page").description("Номер страницы (0-based)"),
                fieldWithPath("size").description("Размер страницы")
            )
        ));
}

Вы заметили, что в этом сниппете мы снова документируем page и size. Это выглядит как дублирование — и оно действительно есть. Но в REST Docs есть баланс: иногда проще (и читаемее) чуть продублировать пару полей, чем городить слишком умные фреймворки и “универсальные сниппеты на все случаи жизни”.

Если хочется аккуратнее, можно вынести общие FieldDescriptor в константу и переиспользовать. Это особенно полезно там, где одни и те же поля pagination или ApiProblem повторяются в нескольких snippets.

Ещё один практический момент: если ваш API эволюционирует и вы добавляете новые поля в ответ (например, content[].readingTimeMinutes), строгое responseFields(...) будет требовать описать каждое поле. Это хорошо, когда вы хотите жёсткий контроль, но иногда нужно документировать только стабильный минимум. Тогда полезен “расслабленный” вариант — relaxedResponseFields(...), который не падает, если есть недокументированные поля.

import org.junit.jupiter.api.Test; 

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.relaxedResponseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;

@Test
void shouldDocumentOnlyStableFields() {
    assertThat(mvc.get().uri("/api/public/articles?page=0&size=20"))
        .hasStatusOk()
        .apply(document("public-articles-list-relaxed",
            relaxedResponseFields(
                // Документируем только "стабильное ядро", остальное игнорируем
                fieldWithPath("content[].slug").description("Публичный идентификатор статьи"),
                fieldWithPath("content[].title").description("Заголовок статьи")
            )
        ));
}

Это не “читерство”. Это осознанный компромисс: вы выбираете, что для вас важнее — строгая полнота документации или устойчивость к добавлению второстепенных полей. Для public API обычно всё-таки лучше держать документацию ближе к контракту, а контракт — стабильным. Но когда вы только начинаете жить с REST Docs, relaxed-режим может спасти психику.

4. Details endpoint: GET /api/public/articles/{slug}

Есть соблазн задокументировать list и details одним сниппетом, потому что “ну там же статья, какая разница”. Разница как минимум в том, что details обычно содержит больше данных, а list — более компактный. И если вы смешаете их в одну «универсальную документацию статьи», клиент будет пытаться использовать list как details или наоборот, а дальше начинается классическое “почему у нас body всегда null?”.

Поэтому мы делаем отдельный сниппет для details endpoint. И сразу появляется ещё одна деталь контракта: path-параметр slug. Даже если вы не используете pathParameters(...) (а он есть в REST Docs), уже по сниппету должно быть понятно, что slug — это ключ запроса и что именно туда нужно подставлять.

Начнём с документации ответа details:

import org.junit.jupiter.api.Test; 

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;

@Test
void shouldDocumentPublicArticleDetails() {
    // Сценарий: клиент открывает страницу конкретной статьи
    assertThat(mvc.get().uri("/api/public/articles/java-testing-basics"))
        .hasStatusOk() // Убедились, что статья доступна и endpoint живой
        .apply(document("public-article-details",
            responseFields(
                // Details обычно богаче, чем list-card, но всё равно документируем по смыслу
                fieldWithPath("slug").description("Публичный идентификатор статьи"),
                fieldWithPath("title").description("Заголовок статьи"),
                fieldWithPath("summary").description("Короткое описание"),
                fieldWithPath("body").description("Полный текст статьи"),
                fieldWithPath("publishedAt").description("Время публикации")
            )
        ));
}

Здесь снова работает правило экономии: не нужно описывать “все поля доменной модели”, если они не являются частью стабильного public контракта. Например, version (optimistic lock) — вообще не про клиента public API. То, что вам важно в базе и JPA, не обязательно важно внешнему потребителю. Документация помогает вам заметить эти смешения: если рука тянется документировать version и authorUsername в публичном endpoint — это хороший момент остановиться и спросить “а клиенту правда это нужно?”.

Если вы хотите добавить документирование path параметра (что обычно полезно), это можно сделать так же “поверх” проверенного сценария:

import org.junit.jupiter.api.Test; 

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;

@Test
void shouldDocumentSlugPathParam() {
    assertThat(mvc.get().uri("/api/public/articles/java-testing-basics"))
        .hasStatusOk()
        .apply(document("public-article-details-path",
            pathParameters(
                // Документируем "ключ" запроса: что именно нужно подставить в URL
                parameterWithName("slug").description("Slug опубликованной статьи")
            )
        ));
}

С точки зрения читателя документации это снимает кучу вопросов: “это id?”, “это UUID?”, “это человекочитаемый путь?”. Мы явно говорим: это slug, публичный идентификатор.

5. Ошибки API: 404 и 400 для пагинации

Ошибка в API — это не “стыдно, спрячем”, а такой же контракт, как и 200 OK. Более того, ошибки часто важнее: happy path клиент может «как-то обработать», а вот если он не понимает, что означает errorCode, он не сможет показать человеку нормальное сообщение и будет просто писать “Oops”.

В ContentHub у нас есть стабильный формат ApiProblem (ProblemDetail-compatible стиль): поля вроде type, title, status, detail, errorCode, плюс иногда violations. Значит, мы можем и должны документировать этот payload. Удобная стратегия — сделать отдельные snippets под разные классы ошибок: не найдено (404), невалидные параметры пагинации (400), и при желании — общий “problem payload overview”.

Документируем 404 “статья не найдена”. С точки зрения теста, нам важно вызвать endpoint с отсутствующим slug и получить статус 404. Как именно сервис к этому пришёл, для REST Docs теста вторично; обычно это решается стабом через @MockitoBean, но документируем мы не мок, а сам ответ на HTTP‑границе.

import org.junit.jupiter.api.Test; 

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;

@Test
void shouldDocumentArticleNotFound() {
    // Сценарий: клиент запросил статью, которой нет
    assertThat(mvc.get().uri("/api/public/articles/missing-slug"))
        .hasStatusNotFound() // Контракт: для отсутствующей статьи возвращаем 404
        .apply(document("public-article-not-found",
            responseFields(
                // Документируем полезный минимум, который нужен клиенту для обработки ошибки
                fieldWithPath("title").description("Короткое имя ошибки"),
                fieldWithPath("status").description("HTTP-статус"),
                fieldWithPath("detail").description("Человеко-читаемое описание"),
                fieldWithPath("errorCode").description("Стабильный код ошибки")
            )
        ));
}

Заметьте: мы не документируем здесь все поля ApiProblem. Это снова осознанная экономия. Например, instance и type бывают полезны, но если они у вас ещё не устоялись или иногда отличаются, лучше не делать их центральными. Вы можете документировать их позже или отметить как optional. В REST Docs fieldWithPath(...).optional() работает и для response fields.

Теперь документируем 400 Bad Request для пагинации, если, например, клиент передал некорректный size. В идеальном мире это будет validation error с violations (например, size должен быть > 0 и <= 100). И вот это как раз то, что клиенту нужно: список нарушений.

import org.junit.jupiter.api.Test; 

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;

@Test
void shouldDocumentInvalidPaginationError() {
    // Сценарий: клиент передал некорректный размер страницы
    assertThat(mvc.get().uri("/api/public/articles?page=0&size=-1"))
        .hasStatusBadRequest() // Контракт: невалидные параметры -> 400
        .apply(document("public-articles-invalid-pagination",
            responseFields(
                // Базовые поля ошибки
                fieldWithPath("status").description("HTTP-статус"),
                fieldWithPath("errorCode").description("Стабильный код ошибки"),
                // Нарушения валидации: клиент может подсветить поле и показать сообщение
                fieldWithPath("violations[].field").description("Поле с нарушением"),
                fieldWithPath("violations[].message").description("Описание нарушения")
            )
        ));
}

Если violations не всегда присутствует (например, для 404 его нет), тогда в “общем” сниппете можно пометить его как optional. Но я бы не делал один “универсальный error snippet на все случаи” сразу, потому что он получается слишком размытым. Практичнее иметь 2–3 конкретных документационных сценария: один для not-found, один для validation, и всё.

В итоге у нас вырисовывается очень здоровая мысль: у одного endpoint’а нормально иметь два документационных сниппета — happy path и error path. Клиенту это часто даже важнее, чем “ещё одно поле в успешном ответе”.

Чтобы закрепить картинку, вот мини-схема того, как это выглядит в голове (и в проекте) на практике:

flowchart TD
    A["MVC-тест (MockMvcTester)"] --> B["assertions: статус, JSON, headers"]
    B --> C["document(...): query params / response fields"]
    C --> D["generated snippets"]
    D --> E["Документация API (собирается из snippets)"]

Важный акцент: документация стоит на плечах проверенного сценария. Мы не рисуем контракт руками; мы вырастаем его из теста, который уже доказал, что контракт реально такой.

6. Живые REST Docs: snippets и стабильные имена

Для public API ContentHub удобнее держаться явных path-like имён snippets: по названию сразу видно endpoint и сценарий. {method-name} остаётся рабочей альтернативой, но здесь важнее, чтобы дерево generated-snippets само рассказывало, что именно задокументировано.

Когда REST Docs только появляется в проекте, первая эмоция часто такая: “О, круто, теперь у нас будет документация!”. Вторая эмоция через неделю: “О нет, теперь у нас ещё и документация…”. Чтобы REST Docs не стал «налогом на жизнь», нужно две вещи: разумно выбирать, что документировать, и сделать snippets переиспользуемыми, но без магии.

Начнём с имён snippets. Самая практичная схема — использовать имена, которые читаются как путь: зона API + endpoint + сценарий. Тогда build/generated-snippets становится почти само-документируемым деревом. Например:

Snippet name Что это
public-articles-list public list happy path
public-article-details public details happy path
public-article-not-found 404 на details
public-articles-invalid-pagination 400 на list

Это не единственно правильный подход, но он помогает не превратить snippets в набор “doc1”, “doc2”, “doc-final-final2”.

Теперь — переиспользование. Самый «безопасный» вариант переиспользования — не прятать весь тест в DSL, а вынести только повторяемые куски: например, описание базовых полей ApiProblem. Тогда документация становится единообразной, а тест остаётся читабельным.

import org.springframework.restdocs.payload.FieldDescriptor;

import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;

// Небольшой хелпер: держим общие FieldDescriptor в одном месте
final class ApiProblemDoc {

    static FieldDescriptor[] basic() {
        return new FieldDescriptor[] {
            // Минимальный контракт ошибки, который мы считаем стабильным
            fieldWithPath("title").description("Короткое имя ошибки"),
            fieldWithPath("status").description("HTTP-статус"),
            fieldWithPath("detail").description("Описание ошибки"),
            fieldWithPath("errorCode").description("Стабильный код ошибки")
        };
    }
}

Дальше вы используете это в тесте и добавляете специфичные поля конкретного сценария, например violations. Да, нужно будет склеить массивы дескрипторов (можно вручную, можно маленьким helper’ом), но идея простая: общий минимум живёт в одном месте, а “специфика” остаётся в тесте.

Ещё один принцип, который спасает от “сниппетов ради сниппетов”: документируйте то, что действительно стабильно и полезно. Если поле вычисляется динамически и его смысл клиенту не нужен, не делайте его центральным в документации. REST Docs — это не обязанность описать каждый байт, это обязанность описать контракт.

И последнее: не бойтесь того, что один endpoint будет иметь несколько snippets. Для клиентов это нормально: один и тот же URL имеет разные сценарии и разные ответы. Документация, которая честно показывает “успешный ответ” и “ошибка” — это взрослая документация. Документация, которая делает вид, что ошибок не существует, — это как инструкция к чайнику без раздела “что делать, если он не включается”.

7. Типичные ошибки в Spring REST Docs

Ошибка №1: документировать до того, как вы проверили контракт assertions.
Очень легко увлечься REST Docs и начать писать document(...) как главную цель теста. В итоге вы документируете сценарий, который на самом деле никак не проверен. Это превращает документацию в красивую теорию. Лекарство простое и скучное: сначала статус, заголовки и смысловые поля ответа проверяются assertions, а уже потом поверх этого цепляется document(...).

Ошибка №2: пытаться описать в документации «весь JSON целиком», включая случайные поля.
Снапшот “весь ответ как есть” выглядит соблазнительно, но он очень быстро ломается от любого рефакторинга и рождает страх менять контракт даже там, где это безопасно. В REST Docs лучше документировать стабильные и полезные поля, а не превращать документацию в музей сериализации. Если хочется частичной документации, используйте relaxed-подход или документируйте только важные куски.

Ошибка №3: документировать только happy path и игнорировать error contract.
На практике клиенты чаще спотыкаются об ошибки, чем о успешные ответы. Если вы задокументировали 200 OK, но не задокументировали ApiProblem для 404 и 400, клиент всё равно будет писать обработку ошибок «по догадке». Потом ваша команда получит вопросы: “а как отличить ARTICLE_NOT_FOUND от ACCESS_DENIED?” и “а violations где?”. Ошибки — часть API, и REST Docs как раз удобен тем, что заставляет это признать.

Ошибка №4: смешивать документацию пагинации и документацию элемента списка так, что никто ничего не понимает.
Если вы в одном сниппете пытаетесь описать и page/size/totalPages, и content[].slug/title/summary, а ещё и вложенные поля категории, документация становится нечитаемой. Лучше документировать pagination wrapper как структуру страницы и отдельно — структуру элемента (article card). Даже если это два snippets, читатель будет вам благодарен.

Ошибка №5: нестабильные имена snippets и “doc-final3” в репозитории.
Поначалу кажется, что имя — мелочь. Потом у вас 40 сниппетов, и вы больше времени тратите на поиск “как же назывался тот сниппет про not found”, чем на реальную работу. Стабильная схема именования (public/list/details/error) — это не бюрократия, а навигация по проекту, когда тестов и документации становится много.

1
Задача
Spring Test, 26 уровень, 1 лекция
Недоступна
Документация пагинации для public list
Документация пагинации для public list
1
Задача
Spring Test, 26 уровень, 1 лекция
Недоступна
Документация 404 error contract для отсутствующей статьи
Документация 404 error contract для отсутствующей статьи
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ