JavaRush /Курсы /Spring Boot /Landing page и границы web-сервиса

Landing page и границы web-сервиса

Spring Boot
10 уровень , 4 лекция
Открыта

1. Landing page в backend-сервисе

Когда web-runtime только поднялся, корень / честно отвечал 404: сервер уже жил, но у него ещё не было ни welcome page, ни доменных маршрутов. Теперь у каталога уже есть рабочие JSON-входы /api/catalog/..., и остался последний штрих — сделать для них человеческую точку входа, чтобы сервис можно было исследовать без угадывания URL.

Landing page для backend-сервиса звучит как маленький парадокс: «Мы же делаем API, зачем нам HTML?» Но в реальной жизни сервис часто проверяют руками, показывают коллеге, демонстрируют студенту или запускают на новой машине. И вот тут простая стартовая страница — это не фронтенд, а удобная табличка «вход здесь», чтобы не играть в квест “угадай endpoint”.

Важно сразу договориться о терминологии. В нашем контексте landing page — это минимальная статическая страница, которая живёт по адресу / и служит навигацией по уже существующим JSON-эндпоинтам. Мы не строим UI, не делаем формы, не рисуем красивую админку. Мы даём сервису “визитку”: название, пару ссылок, возможно одну-две подсказки «как пользоваться».

Такой подход ещё хорош тем, что он честно отражает философию курса. Мы делаем read-only сервис каталога. У него есть web-слой (Spring MVC) и JSON-ответы. Но у него не появляется отдельный фронтенд-проект, не добавляются шаблонизаторы, не появляются «динамические» HTML-страницы, которые нужно рендерить на сервере. Landing page — это просто статический ресурс, который Boot отдаст “как есть”.

2. Static resources и welcome page в Boot

Чтобы landing page была «по-взрослому простой», нам нужно понять один трюк Spring Boot: он умеет раздавать статические файлы прямо из classpath, не заставляя вас писать controller «ради HTML». Это удобно, потому что controller — это всё-таки web-адаптер для бизнес-логики, а выдача файла index.html — это ближе к “ресурсу приложения”, чем к доменной логике. Иначе говоря: файл лучше оставить файлом.

В Boot (со spring-boot-starter-webmvc) есть дефолтная поддержка статических ресурсов. Если файл лежит в стандартных местах внутри src/main/resources, то Boot будет раздавать его по HTTP автоматически. Для нас ключевое место сегодня — src/main/resources/static.

Полезно держать в голове маленькую табличку «откуда берутся статические файлы»:

Где лежит файл (в classpath) Пример файла Какой URL получится
classpath:/static/ static/index.html /index.html и (важно)
/
как welcome page
classpath:/public/ public/logo.png /logo.png
classpath:/resources/ resources/app.css /app.css
classpath:/META-INF/resources/ META-INF/resources/favicon.ico /favicon.ico

Тут есть два важных нюанса, которые любят «кусать» новичков. Первый: если вы положили index.html в static, то Boot может использовать его как welcome page, то есть отдавать по запросу GET /, даже если вы не заходите на /index.html. Второй: статические ресурсы работают по принципу “если нет более конкретного обработчика”. Если вы позже создадите controller-метод с маппингом на /, то уже controller победит, а index.html останется лежать в стороне и грустить. Это нормально: явная логика приложения имеет приоритет над «раздай файл».

Чтобы картинка уложилась в голове, вот упрощённая схема (без внутренностей MVC, только “кто кого вызывает”):

flowchart TD
    A["Браузер: GET /"] --> B["Spring MVC: ищем обработчик"]
    B -->|"Есть controller на '/'"| C["Controller-метод"]
    B -->|"Controller нет"| D["Static resources (index.html)"]
    C --> E["Ответ (JSON/текст/...)"]
    D --> F["Ответ (HTML)"]

В этой лекции мы сознательно выбираем ветку “controller нет, отдаём index.html”, потому что это самый чистый и понятный “вход” в сервис без лишней архитектурной нагрузки.

3. Добавляем static/index.html в catalog-service

Сейчас мы сделаем ровно то, что выглядит слишком простым, чтобы быть полезным — и именно поэтому в реальных сервисах это часто и делают. Мы добавим файл index.html в папку статических ресурсов. После этого при открытии http://localhost:8080/ браузер покажет страницу, где можно кликнуть по ссылкам на наши API. Это буквально «меню ресторана», а не сама кухня.

Создаём файл по пути:

<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8" />
  <!-- Заголовок вкладки браузера: удобно, когда открыто несколько сервисов -->
  <title>catalog-service</title>
</head>
<body>
  <!-- Самый простой «вход» в сервис: человек сразу понимает, куда попал -->
  <h1>catalog-service</h1>
  <p>Минимальный учебный сервис каталога курсов (read-only).</p>

  <h2>API</h2>
  <ul>
    <!-- Абсолютные ссылки (с /) работают независимо от текущего URL -->
    <li><a href="/api/catalog/courses">Все курсы</a></li>
    <li><a href="/api/catalog/featured">Рекомендуемые курсы</a></li>
    <!-- Пример конкретного slug: полезно как «быстрая проверка» -->
    <li><a href="/api/catalog/courses/spring-boot">Курс по slug: spring-boot</a></li>
  </ul>
</body>
</html>

Здесь специально всё «в лоб». Никаких CSS-фреймворков, никакого JavaScript, никаких “красивых карточек”. Нам важна только навигация. Обратите внимание: ссылки абсолютные (начинаются с /). Это удобно, потому что они работают независимо от того, как именно пользователь попал на страницу, и не зависят от текущего URL.

Теперь, если приложение запущено, вы можете открыть корень:

  • GET http://localhost:8080/ → вернётся HTML
  • клик по любой ссылке отправит браузер на соответствующий GET /api/catalog/... маршрут → вернётся JSON

И вот вы уже чувствуете сервис живым: у него есть «вход», есть «коридоры», и пользователь не обязан помнить пути наизусть.

Небольшая, но важная ремарка о соблазнах. У многих начинающих в этот момент возникает мысль: «О, сделаю controller @GetMapping("/") и верну там HTML строкой». Технически можно, но это почти всегда проигрыш: HTML-строка в Java быстро превращается в кашу, а controller перестаёт быть web-адаптером и начинает быть странным генератором разметки. Поэтому статический файл — самый здоровый старт.

Вот пример того, как “не надо” (как минимум на нашем этапе):

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class RootController {

    @GetMapping("/")
    String root() {
        // Важно: @RestController вернёт строку как тело ответа (plain text),
        // а не как «страницу», которую кто-то будет рендерить через view-слой.
        return "<h1>catalog-service</h1>"; // вернётся как текст, не как HTML-страница
    }
}

Почему это плохо? Потому что @RestController пишет результат в тело ответа, и браузер чаще всего покажет вам «сырой текст». А если вы начнёте делать “правильный HTML”, вы внезапно окажетесь в теме шаблонов и view-layer, которую мы в этом курсе сознательно не развиваем. Статический index.html решает задачу без этих побочных эффектов.

4. Навигация должна опираться на уже существующее API

Landing page полезна ровно до тех пор, пока она показывает реальные маршруты, по которым сервис уже умеет отвечать. Иначе вместо “вход здесь” получится коллекция ссылок, половина из которых ведёт в 404 и только путает. Для catalog-service нам сейчас достаточно трёх доменных входов: список курсов, конкретный курс по slug и подборка featured.

Этого набора хватает, чтобы руками проверить весь базовый путь данных: открыть /, перейти на список, выбрать известный slug, увидеть 404 на неизвестный и убедиться, что controller остаётся тонким слоем над service. Поэтому дальше полезно зафиксировать карту сервиса явно — landing page, таблица endpoint’ов и ручные проверки должны смотреть на один и тот же набор маршрутов.

5. Минимальный web-baseline catalog-service

Очень легко, почувствовав вкус к endpoint’ам, начать “пилить ещё чуть-чуть” — и внезапно оказаться в проекте на три месяца. Чтобы этого не случилось, нам важно зафиксировать, что именно является минимальным web-baseline на данном этапе: какие URL уже подняты, какую роль они играют и почему нам этого достаточно, чтобы считать сервис живым. Это момент дисциплины: мы не «недоделали», мы осознанно остановились.

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

Endpoint Что возвращает Зачем он нужен прямо сейчас
GET / index.html Быстрый вход в сервис через браузер, навигация по API
GET /api/catalog/courses List<CourseCard> Базовый read-only список курсов
GET /api/catalog/courses/{slug} 200 + CourseCard / 404 Получение одной карточки по ключу и честный ответ, если такого курса нет
GET /api/catalog/featured List<CourseCard> Маленькая подборка “рекомендуемых”

Пока что это выглядит почти игрушечно, но обратите внимание на ценность. У нас есть один “человеческий вход” (landing page) и три простых JSON-входа. Этого достаточно, чтобы демонстрировать, что Spring Boot поднял web-runtime, что controller-слой работает, что доменная модель уезжает наружу как JSON, и что сервис можно исследовать в браузере, не используя дополнительных инструментов.

Теперь давайте связать это в одну “историю клика”. Пользователь открывает /, видит ссылки, кликает на “Все курсы”, получает JSON. Да, браузер покажет JSON не так красиво, как Postman, но зато этот путь понятен любому человеку. Вот диаграмма этого потока:

flowchart TD
    U["Пользователь в браузере"] --> A["GET /"]
    A --> S["Spring Boot"]
    S --> H["Static resource: index.html"]
    H --> U

    U --> B["Клик по ссылке: /api/catalog/courses"]
    B --> S
    S --> D["DispatcherServlet"]
    D --> C["CourseCatalogController"]
    C --> L["CourseCatalogService"]
    L --> R["InMemoryCourseCatalogRepository"]
    R --> L --> C --> D --> U["JSON ответ"]

Если вы понимаете эту схему — вы уже не «магически вызываете контроллеры», вы реально видите архитектуру: статические ресурсы обслуживаются одним механизмом, JSON-эндпоинты — другим, а бизнес-данные остаются в сервисе/репозитории.

6. Ручная проверка сервиса

Когда сервис только появляется, его чаще всего проверяют “по-бедному”: браузером и curl. И это нормально. На этом этапе нам не нужен красивый API-клиент, нам нужна уверенность: «поднялось», «маршрут есть», «ответ приходит». Главное — не превращать ручную проверку в хаос “я где-то видел этот URL…”.

Самая простая ручная проверка начинается с landing page:

# Проверяем, что корень отвечает HTML и что сервер вообще поднялся
curl -i http://localhost:8080/

Вы увидите статус (скорее всего 200 OK) и HTML в теле ответа. В браузере это будет ещё приятнее, потому что там можно просто кликать ссылки.

Дальше проверяем JSON-эндпоинты. Например, список курсов:

# Проверяем, что endpoint возвращает JSON (и что сериализация работает)
curl -i http://localhost:8080/api/catalog/courses

Если всё хорошо, вы получите Content-Type: application/json и массив объектов. Для новичка важный момент: “массив объектов” не означает, что вы где-то руками склеили JSON. Вы вернули List<CourseCard>, а Spring MVC + Jackson сделали остальное.

И сразу полезно проверить отрицательный сценарий — неизвестный slug:

# Проверяем поведение на несуществующий slug: в финальном baseline это честный 404
curl -i http://localhost:8080/api/catalog/courses/i-do-not-exist

Ожидаемый результат — 404 Not Found. Это и есть нормальный ответ для ресурса, которого нет. Если вместо этого у вас 500, значит в коде ещё осталось старое поведение с исключением, а не канонический вариант с ResponseEntity.notFound().

Этого набора проверок уже достаточно, чтобы увидеть две разные роли web-слоя: корень / отдаёт человеческий вход в сервис, а /api/catalog/... отдаёт доменные JSON-ответы.

7. Граница: Boot-курс и REST-дизайн

Самое сложное в учебном проекте — не написать код, а вовремя перестать писать код. Особенно когда всё начало получаться. Поэтому сейчас важно зафиксировать границу: landing page и несколько read-only эндпоинтов — это web-baseline, а не полноценное API-проектирование. Мы подтверждаем, что сервис стал web-сервисом, но не делаем вид, что уже построили «идеальный REST».

Что мы намеренно не делаем на этом этапе? Мы не добавляем операции записи (POST/PUT/PATCH/DELETE), потому что это мгновенно приводит к большим темам: тело запроса, валидация входных данных, транзакционные границы, ошибки, согласованность, безопасность. Мы также не строим DTO-слой “на всякий случай”: в нашем учебном сервисе read-model (CourseCard) пока достаточно прозрачен, и дополнительные обёртки только ухудшат читабельность.

Мы не подключаем шаблонизаторы (Thymeleaf, Freemarker) и не строим server-side HTML. Если вам хочется «красивую страницу курса по slug», это уже отдельный мир с view-слоем. На данном этапе landing page — это навигация, а не UI-приложение. И, наконец, мы не проектируем единый error-contract и не документируем API через OpenAPI. Это важные вещи, но у них есть свой курс и свой контекст, и они требуют уже более уверенного владения web-слоем.

В результате граница звучит так: в рамках курса Spring Boot мы хотим, чтобы вы уверенно подняли приложение, понимали его wiring и web-baseline, умели отдавать JSON и делать сервис наблюдаемым/управляемым через конфигурацию (к этим темам мы придём позже). А вот превращать проект в “почти prod” REST API мы сейчас не будем. И это не «недоработка» — это методическая чистота. Иначе вместо фундамента получится бесконечная стройка, где фундамент всё время заливают заново.

8. Типичные ошибки при landing page и навигации

Когда впервые добавляешь landing page в Boot-сервис, ошибки обычно очень бытовые и от этого особенно обидные: всё “почти работает”, но на корне 404, или HTML лежит “где-то рядом”, или внезапно вместо страницы приходит JSON-строка. Дальше — самые частые грабли, которые я видел у начинающих, и короткое объяснение, почему так происходит.

Ошибка №1: index.html положили не туда, и Boot его не видит.
Часто файл кладут в src/main/java/... рядом с классами или в корень проекта. Но Spring Boot раздаёт статику из classpath, то есть из src/main/resources (и конкретных стандартных подпапок). Если файла нет в src/main/resources/static/index.html (или другом стандартном месте), то при GET / вы получите 404, и это будет “правильный” 404: сервиса как будто и нет. Лечится очень просто — правильным путём файла и проверкой, что он попал в сборку.

Ошибка №2: сделали controller на / и удивились, что landing page пропала.
Это нормально: явный mapping в MVC обычно имеет приоритет, и статический ресурс больше не используется как welcome page. Особенно часто это происходит, когда пишут @RestController и добавляют @GetMapping("/") “для проверки”. В итоге браузер получает строку (или JSON), а вы ждёте HTML. Решение простое: либо вы действительно хотите controller на / (тогда это уже отдельный дизайн), либо вы оставляете / под статическую welcome page и не трогаете его контроллерами.

Ошибка №3: пытаются “рендерить HTML” из @RestController, возвращая строки.
@RestController по смыслу говорит Spring MVC: “результат метода — это тело ответа”. Поэтому строка становится plain text (или JSON-строкой), а не HTML-страницей. Новичок видит <h1>...</h1> в браузере и думает “почему не отрисовалось?”. Потому что вы не в мире шаблонов и view-resolver’ов, вы в мире @ResponseBody. Если нужен HTML без шаблонов — статические ресурсы; если нужен динамический HTML — это отдельная ветка технологий, которую мы здесь не развиваем.

Ошибка №4: добавили ссылки на endpoints, которых ещё нет, и сами себе устроили 404-квест.
Landing page полезна, когда она честная. Если в index.html вы добавили десять ссылок «на будущее», то браузер превращается в генератор 404, а вы — в человека, который не понимает, что “сломалось”. Лучше держать страницу как «живую карту»: только реально существующие маршруты текущего этапа, и один пример со slug, который точно существует в ваших данных.

Ошибка №5: логика выборки уехала в controller, и controller начал пухнуть.
Это не фатально в учебном коде, но это плохая привычка. Как только controller начинает фильтровать, сортировать, выбирать featured или искать курс по slug сам, он перестаёт быть адаптером и превращается в мини-сервис, только привязанный к HTTP. Потом эту же логику хочется использовать где-нибудь ещё, и начинается копипаста. Правильнее держать controller тонким: взять параметры запроса, вызвать service, вернуть результат.

1
Задача
Spring Boot, 10 уровень, 4 лекция
Недоступна
Welcome page из `static/index.html`
Welcome page из `static/index.html`
1
Задача
Spring Boot, 10 уровень, 4 лекция
Недоступна
Landing page и легкий metadata endpoint
Landing page и легкий metadata endpoint
1
Опрос
Spring MVC, 10 уровень, 4 лекция
Недоступен
Spring MVC
Веб-основа Spring Boot
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ