1. Backend как HTTP-клиент
Если до сих пор backend в голове выглядел как «сервер, который только принимает запросы», то сегодня мы слегка подвинем эту картинку. В реальности backend-сервис почти всегда живёт в окружении других сервисов и API: он кого-то спрашивает, кому-то отправляет данные, у кого-то уточняет детали. И это нормально.
Представьте типичный рабочий день любого «обычного» сервиса: он принял запрос от пользователя, потом сходил во внешний платёжный провайдер, потом уточнил данные доставки в сервисе логистики, потом отправил письмо через почтовый API — и только потом вернул ответ пользователю. Иногда складывается впечатление, что backend на самом деле не «обрабатывает бизнес-логику», а аккуратно дирижирует чужими API (ну, дирижёр тоже профессия, между прочим).
Чтобы сразу поставить понятные термины, полезно различать два направления:
| Направление | Что происходит | Кто «клиент», кто «сервер» |
|---|---|---|
| Inbound HTTP (входящий) | Кто-то стучится к нам | клиент = пользователь/браузер/мобильное приложение, сервер = наш backend |
| Outbound HTTP (исходящий) | Мы стучимся к кому-то | клиент = наш backend, сервер = внешний API/соседний сервис |
Мы сегодня занимаемся именно outbound-частью: наше Java-приложение будет отправлять HTTP-запрос во внешний API и получать ответ. То есть вести себя… как очень серьёзный Postman, только с характером Java и привычкой к строгим типам (пока без типизации — но Java уже морально готовится).
2. Каталог книг для ReadLater Starter
У ReadLater Starter домен простой и человеческий: личный список «прочитать потом». Но как только вы попробуете сделать этот список хоть чуть-чуть удобным, возникает вопрос: откуда брать данные о книге? И тут внешний каталог оказывается не «добавкой», а вполне логичным источником информации.
Можно, конечно, заставить пользователя вручную вводить всё самому: название, автора, год, издательство и ещё желательно «кто переводчик». Но тогда наш сервис быстро превратится в приложение “Угадай, как правильно пишется фамилия автора”, а пользователь начнёт копировать данные из интернета вручную. Это и неудобно, и ошибкоопасно.
Внешний каталог книг в нашем проекте играет роль источника справочных данных. На практике это выглядит так: пользователь вводит поисковую строку (например, clean code), мы отправляем запрос в каталог, получаем список результатов, пользователь выбирает книгу по внешнему идентификатору, и мы можем получить более детальную карточку. Даже если потом у нас появится локальный список чтения, каталог остаётся полезным как «справочник», который умеет находить книги быстрее и точнее, чем человек, уставший после работы.
И критически важный методический момент: мы уже исследовали контракт этого каталога через Postman. То есть сегодня мы не «пробуем угадать правильный URL». Мы делаем инженерно честную вещь: переносим уже проверенный контракт в код. Это прямо как «сначала померили рулеткой, потом распилили», а не наоборот (хотя, да, в программировании иногда всё равно пилят наоборот — но мы попробуем лучше).
3. Локальный vs внешний HTTP-вызов
Внутри Java-программы мы привыкли к очень уютной модели: вызвали метод — получили результат. Всё в одном процессе, один heap, один дебаггер, можно поставить брейкпоинт и почувствовать себя властелином мира. С HTTP-вызовом мозг пытается применить ту же модель — и вот тут обычно начинаются “а почему оно не работает?”.
Для сравнения, вот «обычный» локальный вызов — внутри одного процесса:
// Локальный вызов: всё происходит в нашем процессе, без сети и HTTP-статусов
String result = localCatalog.findById("OL12345M");
// Тут мы печатаем уже готовый результат (например, строку с названием книги)
System.out.println(result); // (например) Clean Code by Robert C. Martin
В этом коде нет ни адреса, ни сети, ни статуса, ни заголовков. Если метод сломается, вы (скорее всего) быстро поймёте где именно: дебаггер, стек-трейс, всё рядом.
А теперь — намёк на внешний вызов. Даже не выполняем его, просто посмотрите на форму:
import java.net.URI;
import java.net.http.HttpRequest;
// Адрес внешнего API: это уже не "вызов метода", а конкретный URL/URI
URI uri = URI.create("https://catalog.example/books/OL12345M");
HttpRequest request = HttpRequest.newBuilder(uri)
// Заголовок: явно говорим, что хотим получить JSON
.header("Accept", "application/json")
// HTTP-метод: читаем данные (а не изменяем)
.GET()
.build();
Тут сразу видна «другая вселенная». Появился адрес URI, появился HTTP-метод, появился заголовок Accept. И главное: даже если запрос выглядит правильным, это ещё не означает успех. Сервер может вернуть 404, может вернуть 500, может вернуть неожиданный формат ответа, а может вообще… не ответить. И это не «экзотика». Это нормальная жизнь сети.
Если хочется зафиксировать различия в одном месте, вот очень практическая табличка:
| Характеристика | Локальный вызов метода | HTTP-вызов во внешний API |
|---|---|---|
| Где выполняется код | В вашем процессе | На чужом сервере |
| Входные данные | Параметры метода | URI + method + headers (+ иногда body) |
| Результат | Возвращаемое значение | Status + headers + body |
| Ошибка | Исключение в вашем коде | Может быть и исключение (сеть), и корректный HTTP-ответ с ошибочным статусом |
| «Можно ли дебажить внутрь?» | Обычно да | Нет (это не ваш код) |
И вот отсюда рождается ключевая привычка backend-разработчика: HTTP-вызов всегда нужно мыслить как контрактное взаимодействие, а не как «метод, который где-то там выполняется».
4. HTTP-контракт внешнего вызова
Когда начинаешь работать с внешним API, очень хочется думать: «ну там же JSON, сейчас как-нибудь прочитаем». Но правильнее думать иначе: «я заключаю договор с чужим сервисом». В этом договоре есть чёткие пункты: куда и как я стучу, и что мне обещают вернуть в ответ. Иначе вы быстро окажетесь в ситуации “оно вчера работало, а сегодня нет”.
В HTTP-контракте есть несколько обязательных частей. Мы их уже разбирали раньше, но сегодня это важно именно с позиции исходящего вызова (то есть когда клиент — это мы):
| Часть контракта | Где это проявляется в коде | Вопрос, который стоит себе задать |
|---|---|---|
| URI | URI.create(...) | Куда я иду? Это точный адрес? |
| Method | .GET(), .POST(), … | Что я делаю: читаю или изменяю? |
| Headers | .header("Accept", "...") | В каком формате я хочу ответ? |
| Status code | response.statusCode() | Успех это или ошибка? Какой класс проблемы? |
| Body | response.body() | Что именно вернулось? Похоже ли на то, что мы ожидали? |
И вот важная мысль: контракт — это не только body. Новичок часто смотрит только на JSON и забывает, что 200 и 404 — это принципиально разные истории. Даже если в обоих случаях тело ответа похоже на JSON (а иногда так и бывает), смысл совершенно разный.
Если совсем приземлённо: внешний API — это как «чек в магазине». Вам важно не только то, что внутри пакета (body), но и что написано сверху: успешно ли прошла покупка (status), какие условия (headers), и вообще правильный ли адрес магазина (URI). Иначе вы рискуете принести домой не продукты, а чувство стыда и набор ошибок.
5. Postman: черновик кода
Хорошая новость: сегодня нам не нужно играть в «угадай правильный endpoint». Мы уже ходили в каталог через Postman, уже видели примеры запросов и ответов, уже проверяли варианты. То есть мы делаем то, что в реальной разработке называется “сперва исследование контракта, потом реализация”.
Удобно мысленно представить, что Postman-запрос — это черновик будущего кода. Примерно так (условный формат, но смысл узнаваемый):
# Поисковый запрос к каталогу (черновик из Postman)
GET {{baseUrl}}/search?q=clean%20code
# Говорим серверу, что ожидаем JSON в ответе
Accept: application/json
Это не просто «красивые строчки». Это зафиксированная договорённость: метод GET, конкретный путь, query-параметр q, и ожидание JSON-ответа.
И вот почему этот шаг важен методически. Когда вы пишете первый HTTP-клиент в Java, очень легко свалиться в магию: «ну оно как-то отправляет запрос». Postman возвращает вас на землю: там всё видно. Метод виден. URL виден. Заголовки видны. Ответ виден. И задача сегодняшнего дня — перенести эту прозрачность в код, а не спрятать её под ковёр «универсального клиента на будущее».
6. Минимальный успех: статус и raw body
На первом шаге очень полезно снизить ожидания до правильного минимума. Мы не строим сегодня идеальную архитектуру, не «проектируем клиентский SDK», не превращаем JSON в красивые Java-объекты. Наш критерий успеха проще и честнее: приложение отправило запрос, получило ответ и смогло показать нам его смысл.
Тут появляется термин, который мы будем использовать весь уровень: raw body. Это тело ответа, прочитанное «как есть», без маппинга в DTO. То есть по сути JSON-строка. И да, для Java-разработчика это звучит как “фу, строка”, но в учебном шаге это именно то, что нужно: сначала убедиться, что транспорт работает и контракт совпадает, а уже потом думать о типах.
Схематично наш сегодняшний минимальный поток выглядит так:
sequenceDiagram
participant U as "Пользователь"
participant A as "ReadLater Starter (Java)"
participant C as "Внешний каталог книг"
Note over U,A: "Пользователь запускает команду и ожидает наблюдаемый результат"
U->>A: "Запуск команды 'catalog search ...'"
Note over A,C: "Важно видеть контракт: URI, метод, заголовки, статус и body"
A->>C: "HTTP GET (URI + Accept)"
C-->>A: "HTTP response (status + headers + JSON body)"
A-->>U: "Печать статуса и raw JSON (preview)"
Обратите внимание, что «ответ» тут — это не только JSON. Даже на уровне “просто вывести в консоль” мы хотим видеть минимум три вещи: статус, заголовки (хотя бы раз посмотреть) и тело. Потому что именно так backend-разработчик учится не терять важные части контракта.
7. Повторяемый шаг
Очень важно, чтобы первый внешний вызов не остался «кодом в отдельном методе, который никто никогда не вызывает». В конце дня вы должны уметь запустить приложение и получить наблюдаемый результат: внешний запрос действительно ушёл, ответ действительно пришёл, и вы действительно видите JSON. Это превращает тему из «теории про HTTP» в привычку.
И при этом мы сознательно не усложняем. Сейчас будет достаточно даже такого «скелета взаимодействия» (пока без деталей реализации, просто как идея):
// Выполняем запрос в каталог и забираем "сырой" JSON как строку
String json = catalogClient.searchRaw("clean code");
// Для первого шага удобно печатать превью, чтобы не завалить консоль мегабайтами JSON
int previewLength = Math.min(json.length(), 200);
System.out.println(json.substring(0, previewLength) + "...");
// { "docs": [ ... ] }...
Этот кусочек важен не тем, что он «красивый», а тем, что он делает внешнюю интеграцию частью приложения. То есть мы можем повторять запуск, сравнивать ответы, понимать, что меняется, и постепенно приводить код в порядок. Это ровно тот же принцип, по которому мы раньше фиксировали Gradle Wrapper и команды запуска: если шаг можно повторить, значит он реально существует, а не «случайно заработал один раз на моём компьютере».
И да, в этот момент может появиться желание немедленно завернуть всё в десять слоёв абстракций. Это нормальное желание (особенно если вы видели красивые архитектурные картинки). Но сегодня наша задача наоборот: оставить механику видимой, чтобы мозг привык к реальности HTTP-вызова. Красоту и «взрослые слои» мы будем наращивать позже — когда будет, что наращивать.
8. Типичные ошибки при первом outbound HTTP-вызове
Когда вы только начинаете делать исходящие HTTP-вызовы, вы неизбежно сравниваете их с привычными вызовами методов и ожидаете такого же поведения. Отсюда рождаются ошибки, которые на практике встречаются даже у умных людей — просто потому, что «мозг ещё не принял правила игры». Лучше поймать их на берегу.
Ошибка №1: думать, что backend “только принимает запросы”, а “ходить наружу” — это что-то редкое.
Из-за этой установки внешний вызов воспринимается как странная разовая фича, а не как нормальная часть системы. В результате код лепится «куда придётся», без дисциплины. Полезно сразу принять: outbound HTTP — это такая же обычная рутина, как работа со строками (к сожалению).
Ошибка №2: мысленно приравнять HTTP-вызов к someService.method() и игнорировать контракт.
Как только вы забываете, что у HTTP есть адрес, метод, заголовки и статус-коды, вы начинаете смотреть только на body. Это опасно: вы можете получить ошибочный статус и всё равно пытаться читать тело как «успешный ответ». Правильная привычка: сначала статус, потом всё остальное.
Ошибка №3: спрятать первый вызов за “универсальным клиентом на будущее”.
Это тот случай, когда «архитектура» убивает обучение. Если на первом шаге вы создадите пять классов и три интерфейса, вы потеряете самое ценное — понимание, как реально выглядит запрос и ответ. На стартовом этапе лучше один маленький понятный вызов, чем «вечный двигатель» из абстракций.
Ошибка №4: писать код без опоры на уже исследованный Postman-контракт.
Иногда хочется: “ну сейчас быстро накидаю URL строкой”. В итоге вы тратите время на угадывание, а не на обучение. Если контракт уже исследован, используйте его как карту. Postman-коллекция — это не архив “на память”, а рабочая документация для кода.
Ошибка №5: считать raw JSON в консоли «финалом задачи».
Raw JSON — отличный промежуточный результат, но только промежуточный. Если вы остановитесь здесь, приложение останется на уровне “печатаем непонятный текст”. Сегодня raw — это инструмент проверки контракта и транспорта. Дальше (не сегодня) мы будем делать ответ пригодным для реального использования в коде.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ