1. Внешний API
Если вы впервые изучаете backend-инструменты, внешний сервис кажется идеальным: он “настоящий”, отвечает по HTTP, присылает реальный JSON, и вообще создаёт ощущение работы «как в проде». Но у учебного проекта есть особое требование: он должен быть повторяемым. То, что работало вчера вечером, должно работать завтра утром — без условий “если провайдер не обновился”.
Давайте честно: внешний API — это чужой код, чужие релизы и чужие решения. Он может поменять структуру ответа, переименовать поле, начать требовать новый параметр, добавить rate limit, временно лечь или начать отвечать заметно медленнее. И в реальной разработке мы с этим живём, да, но в обучении это превращается в лотерею: студент пытается понять Postman и HTTP, а вместо этого попадает в квест «угадай, почему сегодня 503».
Поэтому нам нужен “якорь”, который не плавает. Такой якорь — это зафиксированная форма контракта в виде артефактов: коллекции Postman, окружения с переменными, negative-path запросы и… sample JSON. Он позволяет учиться на стабильном материале, а не на случайных сегодняшних ответах сервиса.
Sample JSON: роль в контракте
Sample JSON часто воспринимают как “ну я куда-то сохранил пример ответа, чтобы не забыть”. Но полезный sample — это не сувенир, а рабочий артефакт контракта. Он отвечает на очень практический вопрос: какой формы данные мы ожидаем увидеть, независимо от того, есть ли сейчас доступ к живому сервису.
Представьте, что HTTP-контракт — это как правила настольной игры. Postman-запрос — это ваша попытка сыграть партию. А sample JSON — это как фотография поля, карточек и фишек, сделанная в момент, когда всё ещё работало и никто не потерял кубик под диваном. Она помогает восстановить “картину мира”: какие поля есть, как они вложены, что бывает массивом, что строкой, где null, а где поля вообще нет.
Важно: sample JSON должен фиксировать структуру, а не “красивые данные”. Нам не важно, что книга называется именно Clean Code (хотя она, конечно, хорошая), нам важно, что в ответе поиска есть список результатов, у элемента есть идентификатор, заголовок и (возможный) автор, а в ответе деталей поля могут быть богаче. Если вы сохраните структуру, вы сможете писать код и проверки на контракт даже тогда, когда внешний сервис внезапно недоступен.
2. Какие ответы сохранять
Хочется сохранить вообще всё: каждый ответ, каждый вариант, каждый случайный эксперимент. Но это путь к папке samples/, которая через три дня выглядит как «архив древней цивилизации», где никто не знает, какой файл главный и почему их 47. Нам нужен минимальный, но осмысленный набор sample-ответов, который покрывает ключевые сценарии каталога и помогает держать контракт в голове.
Для проекта ReadLater Starter в каталожной части у нас есть два базовых сценария: поиск и детали. В терминах Postman это обычно два запроса: search и details. Поэтому базовый набор sample JSON обычно строится вокруг четырёх ситуаций: успешный поиск (есть результаты), пустой поиск (результатов нет, но контракт корректный), успешные детали по найденному ID и детальный запрос по заведомо несуществующему ID (чтобы увидеть, это 404, 200 с пустым телом, 200 с ошибкой в JSON, или ещё что-то).
Для not-found сценария удобно использовать тот же missingBookId, который уже живёт в окружении Postman. Тогда один и тот же маркер работает и в negative-запросе, и в наборе sample-артефактов.
Ниже — пример того, как можно оформить этот минимум в виде таблицы. Это не “официальный стандарт”, а удобная дисциплина, чтобы вы и через неделю понимали, зачем файл существует:
| Сценарий | HTTP-запрос | Ожидаемый статус | Файл sample JSON (идея имени) | Зачем сохраняем |
|---|---|---|---|---|
| Search success | GET /search?q=...&limit=... | 200 | search-success.json | фиксируем форму “списка результатов” |
| Search empty | GET /search?q=... | 200 | search-empty.json | фиксируем форму “пустого списка” |
| Details success | GET /books/{id} | 200 | details-success.json | фиксируем форму карточки книги |
| Details not found | GET /books/{{missingBookId}} | 404 или |
details-not-found.json (опционально, если есть JSON body) | фиксируем, как API сообщает “не найдено” |
Обратите внимание на важную мысль: не каждый negative-path — это ошибка с JSON-body. Иногда API говорит “не найдено” просто статусом 404 и пустым телом. Тогда сохранить “sample JSON” буквально нечего — но сохранить наблюдение о контракте всё равно полезно (например, в README рядом с samples). Мы тут не делаем бюрократию, мы сохраняем понимание.
3. Выбор sample JSON: структура важнее данных
На этом месте возникает очень типичная ловушка новичка: «Я нашёл ответ покрасивее, выкинул половину полей, переименовал другие, чтобы было понятнее, и вот мой sample». Это звучит удобно, но это как переписать дорожные знаки “для удобства” и потом удивляться, почему навигатор сошёл с ума.
Хороший sample JSON должен быть максимально близок к реальному контракту. Если в ответе есть вложенность, пусть она останется. Если в ответе есть странные имена полей — пусть останутся (мы не обязаны любить их, но обязаны уважать). Если в ответе иногда встречается null, полезно сохранить именно такой пример: он показывает, что поле может отсутствовать по смыслу, и это часть контракта. Если бывают массивы, лучше сохранить пример, где массив не пустой, потому что это делает структуру очевиднее.
При этом sample не должен превращаться в «слепок всего мира». Обычно в реальном ответе могут быть “шумовые” поля: рекламные метки, технические поля, локализация, длинные описания на 5000 символов. Для учебного sample важны поля, которые будут участвовать в нашей логике, и поля, которые влияют на форму DTO. Поэтому компромисс такой: мы сохраняем репрезентативный ответ, но не обязаны тащить гигантские куски текста, если они не помогают понять структуру. Если поле очень длинное и мешает читать sample, его допустимо сократить, но делать это нужно осторожно и честно, не ломая тип и вложенность.
Чтобы зафиксировать этот компромисс, полезно держать в голове простое правило: sample JSON должен позволить вам ответить на вопросы “какие поля есть?”, “каких типов они бывают?”, “как они вложены?” и “какие поля могут быть пустыми?”. Если после сохранения sample вы всё равно не можете уверенно сказать, где лежит id, то sample получился скорее декоративным.
4. Сохранение sample JSON из Postman
Переходим к механике. Здесь важно не то, какой именно пункт меню нажать (Postman периодически меняет интерфейс), а чтобы у вас был повторяемый ритуал, который не требует “каждый раз вспоминать, как я это делал”.
Обычно workflow выглядит так. Вы отправляете запрос, получаете корректный ответ и убеждаетесь, что это именно тот сценарий, который вы хотите зафиксировать (например, search success с понятным items[0].id). Затем вы берёте JSON из Response Body и сохраняете его в отдельный файл .json в репозитории. В самом простом варианте это просто copy/paste в IDE. Это нормально, это не “стыдно”, это учебный проект. Главное — не копировать вместе с этим куски HTML или какие-то “представления”, а именно чистый JSON.
Самый практичный вариант здесь — сохранять такие ответы в отдельные .json-файлы внутри репозитория. Именно на них потом удобно опираться в mock-режиме и при работе из Java-кода. Example в самой коллекции можно держать дополнительно как визуальную подсказку, но не как замену файлам в репозитории.
Вот как может выглядеть один из наших sample JSON для поиска (упрощённый и короткий — в реальности полей может быть больше):
{
// Список найденных сущностей (в учебном проекте — книги)
"items": [
{
// Идентификатор, который дальше пойдёт в details-запрос
"id": "BK-101",
// Человекочитаемое название
"title": "Clean Code",
// Может быть строкой, а в других API — объектом или null: это часть контракта
"author": "Robert C. Martin"
}
],
// Общее количество найденных элементов (часто нужно для пагинации)
"count": 1
}
А вот пример для деталей:
{
// Тот же id, что приходит из поиска
"id": "BK-101",
"title": "Clean Code",
"author": "Robert C. Martin",
// Числовое поле: важно сохранить именно число, а не строку
"year": 2008,
// Внешняя ссылка на карточку у провайдера (в mock-режиме она просто строка)
"externalUrl": "https://catalog.example.com/books/BK-101"
}
И пример пустого результата (важно, что структура та же, только данных нет):
{
// Пустой список — это не ошибка, а нормальная ветка контракта
"items": [],
"count": 0
}
Если ваш реальный API отдаёт другой формат (например, docs вместо items, или numFound вместо count), то в sample нужно сохранить его формат. Мы не “исправляем провайдера”, мы учимся работать с контрактом как с фактом.
Ещё одна практичная мелочь: когда вы сохранили sample, очень полезно сразу подписать в Postman запрос так, чтобы по названию было видно, какой sample он породил. Иначе вы через несколько дней откроете коллекцию, увидите “Search test”, а рядом файл search-success.json и будете играть в “угадай пару”.
5. Хранение sample JSON в репозитории
Когда вы храните sample JSON “где-то рядом”, всегда есть риск, что через пару дней они окажутся “где-то не рядом”. Поэтому нам нужна простая, человеческая структура папок. Она должна быть настолько очевидной, чтобы даже вы в уставшем состоянии могли найти нужный файл. (Особенно вы. Именно в уставшем состоянии мы и читаем чужие репозитории.)
На этом этапе курса у нас уже есть Gradle-проект и src/main/resources, и этого достаточно для стабильных ресурсов. Чтобы это не расползалось по репозиторию, проще сразу выбрать одно место и дальше держаться его: sample JSON живут в src/main/resources/mock/catalog/.
# mock/ — данные для подмены внешнего API, а не “бизнес-логика”
# catalog/ — конкретный внешний провайдер (каталог книг)
readlater-starter
└─ src/main/resources/mock/catalog
├─ search-success.json
├─ search-empty.json
├─ details-success.json
└─ details-not-found.json # опционально: только если провайдер реально отдаёт JSON body
Если провайдер на {{missingBookId}} отдаёт только статус и пустое тело, не придумывайте фиктивный details-not-found.json ради симметрии. Достаточно короткой пометки рядом с файлами или в README, что not-found здесь фиксируется статусом и отсутствием body.
Заметьте, здесь есть два слоя смысла. Папка mock/ говорит: это данные не “для прод-логики”, а для учебной подмены. Папка catalog/ говорит: это относится к внешнему каталогу, а не к нашему локальному API. И имена файлов говорят сценарий человеческим языком, без final2_reallyfinal.json.
Если вам хочется ещё чуть больше ясности (и это иногда реально помогает), можно добавить рядом короткий текстовый файл README.md или notes.md прямо в mock/catalog/, где в паре строк написать, откуда взяты ответы и что в них важно. Это не бюрократия — это страховка от собственной забывчивости. Наш мозг — отличный инструмент, но он любит “освобождать оперативку”.
Имена вокруг этих артефактов тоже лучше держать стабильными: baseUrl, searchQuery, limit, bookId, missingBookId. Тогда запросы, sample-файлы и будущий mock-режим говорят на одном языке.
6. Mock-режим: определение и границы
Слово “mock” в IT может означать десятки разных вещей: mock в тестах, mock server, заглушки, фейковые реализации, имитация сети и так далее. Чтобы не устроить терминологическую кашу, договоримся о смысле в рамках этого курса. Mock-режим у нас — это когда проект может работать, опираясь на заранее сохранённые ответы, а не на живой внешний сервис.
То есть мы не пытаемся “воспроизвести интернет”. Мы делаем гораздо проще: вместо реального HTTP-запроса к провайдеру мы берём сохранённый JSON и ведём себя так, как будто это ответ сервиса. Это даёт две учебные выгоды: во‑первых, курс перестаёт зависеть от доступности внешнего API; во‑вторых, мы можем учиться маппить JSON и проектировать DTO на стабильном материале.
Чтобы визуально это закрепить, полезно посмотреть на два потока рядом:
flowchart TD
%% A — точка входа: где пользователь “дергает” наш сценарий
A["Наше приложение / Postman-сценарий"] --> B{"Режим"}
%% real: идём в сеть к провайдеру
B -->|real| C["HTTP-запрос к внешнему каталогу"]
C --> D["Response: status + JSON"]
%% mock: сеть не трогаем, читаем сохранённый пример
B -->|mock| E["Чтение sample JSON из репозитория"]
E --> F["Тот же JSON по форме"]
%% Дальше логика приложения одинаковая: работаем с контрактом/DTO
D --> G["Дальше мы работаем с контрактом"]
F --> G["Дальше мы работаем с контрактом"]
И вот границы, которые важно не пересекать прямо сейчас. Мы сегодня не поднимаем отдельный mock server в Postman, не строим сложную инфраструктуру подмены и не пытаемся автоматизировать всё на свете. Наша цель скромнее и практичнее: подготовить sample-артефакты, которые сделают следующий этап курса (Java-клиент) стабильным, а текущий этап (понимание контракта) — не зависящим от “поведения внешнего мира”.
7. Типичные ошибки при работе с sample JSON
Ошибка №1: сохраняют “красивый” JSON, а не реальный контракт.
Очень хочется сделать ответ удобнее: переименовать поля, удалить вложенность, “причесать” структуру. Но потом вы пишете код или Postman-скрипт и внезапно понимаете, что работаете не с контрактом, а с фанфиком по контракту. Sample должен быть максимально похож на реальный ответ, иначе он перестаёт быть опорой.
Ошибка №2: сохраняют только happy-path и удивляются, что mock-режим не помогает.
Если у вас есть только search-success.json и details-success.json, то любой пограничный случай превращается в импровизацию. Пустой поиск — это не экзотика, а нормальная ветка. Если ваш внешний API на пустой запрос возвращает ошибку или пустой список, это важно зафиксировать отдельно: и в Postman, и в samples.
Ошибка №3: складывают samples в одну “помойную” папку без структуры и смысловых имён.
Папка samples/ с файлами 1.json, new.json, copy.json — почти гарантированная потеря смысла. Через несколько дней вы не сможете ответить, какой файл относится к поиску, какой к деталям, и какой вообще актуален. Имена и папки — это часть контракта, только “для людей”.
Ошибка №4: сохраняют гигантские ответы целиком и делают samples нечитаемыми.
Если провайдер присылает огромные описания, списки на сотни элементов и десятки второстепенных полей, sample превращается в простыню. В учебном режиме ценнее читаемость структуры. Можно выбрать ответ с маленьким limit, а иногда допустимо сократить очень длинное текстовое поле, но так, чтобы не поменялся тип поля и не исчезла вложенность.
Ошибка №5: путают “контракт сломан” и “сервис недоступен”.
Когда API отвечает 502 или вообще не отвечает, у новичка часто появляется мысль “наверное, я неправильно составил запрос”. Иногда да, но часто это просто внешний мир. Sample JSON и mock-режим нужны именно затем, чтобы можно было продолжать учиться контракту даже тогда, когда внешняя сторона временно не в форме.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ