JavaRush /Курсы /Java Server /Внешнее HTTP API в ReadLater Starter

Внешнее HTTP API в ReadLater Starter

Java Server
14 уровень , 0 лекция
Открыта

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 — это инструмент проверки контракта и транспорта. Дальше (не сегодня) мы будем делать ответ пригодным для реального использования в коде.

1
Задача
Java Server, 14 уровень, 0 лекция
Недоступна
Локальный вызов и внешний контракт
Локальный вызов и внешний контракт
1
Задача
Java Server, 14 уровень, 0 лекция
Недоступна
Команда `catalog search` как описание исходящего запроса
Команда `catalog search` как описание исходящего запроса
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ