CodeGym /Kursy /Spring Boot /Strona startowa i granice serwisu web

Strona startowa i granice serwisu web

Spring Boot
Poziom 10, Lekcja 4
Dostępny

1. Landing page w serwisie backendowym

Gdy runtime web właśnie się podniósł, korzeń / uczciwie odpowiadał 404: serwer już żył, ale nie miał jeszcze ani welcome page, ani tras domenowych. Teraz katalog ma już działające wejścia JSON /api/catalog/... i pozostaje ostatni szlif – zrobić dla nich ludzki punkt wejścia, żeby serwis można było badać bez zgadywania URL.

Landing page dla serwisu backendowego brzmi jak mały paradoks: „Przecież robimy API, po co nam HTML?" Ale w prawdziwym życiu serwis często sprawdza się ręcznie, pokazuje się współpracownikowi, demonstruje studentowi lub uruchamia na nowej maszynie. I tu prosta strona startowa – to nie frontend, lecz wygodna tabliczka „wejście tutaj", żeby nie grać w quest „zgadnij endpoint".

Ważne jest od razu ustalenie terminologii. W naszym kontekście landing page – to minimalna statyczna strona, która żyje pod adresem / i służy jako nawigacja po już istniejących endpointach JSON. Nie budujemy UI, nie robimy formularzy, nie rysujemy pięknego panelu admina. Dajemy serwisowi „wizytówkę": nazwa i kilka linków, ewentualnie jedna-dwie podpowiedzi „jak korzystać".

Takie podejście jest dobre jeszcze dlatego, że uczciwie odzwierciedla filozofię kursu. Robimy serwis read-only katalogu. Ma on warstwę web (Spring MVC) i odpowiedzi JSON. Ale nie pojawia się osobny projekt frontend, nie są dodawane silniki szablonów, nie pojawiają się „dynamiczne" strony HTML wymagające renderowania po stronie serwera. Landing page – to po prostu statyczny zasób, który Boot odda „jak jest".

2. Static resources i welcome page w Boot

Żeby landing page była „po dorosłemu prosta", trzeba zrozumieć jeden trik Spring Boot: potrafi serwować statyczne pliki wprost z classpath, nie zmuszając Cię do pisania kontrolera „dla HTML". To wygodne, bo kontroler – to jednak adapter web dla logiki biznesowej, a wydawanie pliku index.html – to bliżej „zasobu aplikacji" niż logiki domenowej. Inaczej mówiąc: plik lepiej zostawić plikiem.

W Boot (ze spring-boot-starter-webmvc) jest domyślne wsparcie dla statycznych zasobów. Jeśli plik leży w standardowych miejscach wewnątrz src/main/resources, Boot będzie go serwować przez HTTP automatycznie. Dla nas kluczowym miejscem dziś jest src/main/resources/static.

Przydatna jest mała tabela „skąd biorą się statyczne pliki":

Gdzie leży plik (w classpath) Przykład pliku Jaki URL wychodzi
classpath:/static/ static/index.html /index.html i (ważne)
/
jako 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

Są tu dwa ważne niuanse, które lubią „gryźć" nowicjuszy. Pierwszy: jeśli umiesciłeś index.html w folderze static, Boot może użyć go jako welcome page, czyli oddawać na żądanie GET /, nawet jeśli nie wchodzisz na /index.html. Drugi: statyczne zasoby działają na zasadzie „jeśli nie ma bardziej konkretnego handlera". Jeśli później stworzysz metodę kontrolera z mappingiem na /, to kontroler wygra, a index.html zostanie na uboczu i będzie smucić. To normalne: jawne zachowanie ma priorytet nad niejawnym.

Żeby obraz się ułożył w głowie, uproszczony schemat (bez wnętrz MVC, tylko „kto kogo wywołuje"):

flowchart TD
    A["Przeglądarka: GET /"] --> B["Spring MVC: szukamy handlera"]
    B -->|"Jest kontroler na '/'"| C["Metoda kontrolera"]
    B -->|"Kontrolera nie ma"| D["Static resources (index.html)"]
    C --> E["Odpowiedź (JSON/tekst/...)"]
    D --> F["Odpowiedź (HTML)"]

W tej lekcji świadomie wybieramy gałąź „kontrolera nie ma, serwujemy index.html", bo to najczystsze i najzrozumialsze „wejście" do serwisu bez zbędnego obciążenia architektonicznego.

3. Dodajemy static/index.html do catalog-service

Teraz zrobimy dokładnie to, co wydaje się zbyt proste, żeby być przydatne – i właśnie dlatego w prawdziwych serwisach często tak robią. Dodamy plik index.html do folderu zasobów statycznych. Po tym przy otwarciu http://localhost:8080/ przeglądarka pokaże stronę, gdzie można klikać w linki do naszych API. To dosłownie „menu restauracji", a nie kuchnia.

Tworzymy plik pod ścieżką:

<!doctype html>
<html lang="pl">
<head>
  <meta charset="utf-8" />
  <!-- Tytuł zakładki przeglądarki: wygodne, gdy otwartych jest kilka serwisów -->
  <title>catalog-service</title>
</head>
<body>
  <!-- Najprostsze "wejście" do serwisu: człowiek od razu rozumie, gdzie trafił -->
  <h1>catalog-service</h1>
  <p>Minimalny dydaktyczny serwis katalogu kursów (read-only).</p>

  <h2>API</h2>
  <ul>
    <!-- Linki bezwzględne (z /) działają niezależnie od bieżącego URL -->
    <li><a href="/api/catalog/courses">Wszystkie kursy</a></li>
    <li><a href="/api/catalog/featured">Kursy rekomendowane</a></li>
    <!-- Przykład konkretnego slug: przydatny jako "szybkie sprawdzenie" -->
    <li><a href="/api/catalog/courses/spring-boot">Kurs po slug: spring-boot</a></li>
  </ul>
</body>
</html>

Tu celowo wszystko „wprost". Żadnych frameworków CSS, żadnego JavaScript, żadnych „pięknych kart". Potrzebna jest tylko nawigacja. Zwróć uwagę: linki są bezwzględne (zaczynają się od /). To wygodne, bo działają niezależnie od tego, jak dokładnie użytkownik trafił na stronę, i nie zależą od bieżącego URL.

Teraz, jeśli aplikacja jest uruchomiona, możesz otworzyć korzeń:

  • GET http://localhost:8080/ → zwróci HTML
  • kliknięcie w dowolny link wyśle przeglądarkę na odpowiedni GET /api/catalog/... → zwróci JSON

I tak już czujesz serwis jako żywy: ma „wejście", ma „korytarze" i użytkownik nie musi pamiętać ścieżek na pamięć.

Mała, ale ważna uwaga o pokusach. U wielu początkujących w tym momencie pojawia się myśl: „O, zrobię kontroler @GetMapping("/") i tam zwrócę HTML ciągiem". Technicznie się da, ale to prawie zawsze porażka: ciąg HTML w Javie szybko zamienia się w bagno, a kontroler przestaje być adapterem web i zaczyna być dziwnym generatorem znaczników. Dlatego statyczny plik – to najzdrowszy start.

Oto przykład tego, jak „nie trzeba" (przynajmniej na naszym etapie):

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

@RestController
class RootController {

    @GetMapping("/")
    String root() {
        // Ważne: @RestController zwróci ciąg jako treść odpowiedzi (plain text),
        // a nie jako "stronę", którą ktoś będzie renderować przez warstwę view.
        return "<h1>catalog-service</h1>"; // zwróci się jako tekst, nie jako strona HTML
    }
}

Dlaczego to jest złe? Bo @RestController pisze wynik do treści odpowiedzi i przeglądarka najczęściej pokaże Ci „surowy tekst". A jeśli zaczniesz robić „prawdziwy HTML", nagle znajdziesz się w temacie szablonów i warstwy view, których w tym kursie świadomie nie rozwijamy. Statyczny index.html rozwiązuje zadanie bez tych efektów ubocznych.

4. Nawigacja powinna opierać się na istniejącym API

Landing page jest przydatna dokładnie do momentu, gdy pokazuje prawdziwe trasy, na które serwis już potrafi odpowiadać. Inaczej zamiast „wejście tutaj" dostaniemy kolekcję linków, z których połowa prowadzi do 404 i tylko myli. Dla catalog-service teraz wystarczają trzy domenowe wejścia: lista kursów, konkretny kurs po slug i podborka featured.

Ten zestaw wystarczy, żeby ręcznie sprawdzić całą podstawową ścieżkę danych: otworzyć /, przejść do listy, wybrać znany slug, zobaczyć 404 na nieznany i upewnić się, że kontroler pozostaje cienką warstwą nad serwisem. Dlatego dalej warto jawnie utrwalić mapę serwisu – landing page, tabela endpointów i ręczne sprawdzenia powinny patrzeć na ten sam zestaw tras.

5. Minimalny web-baseline catalog-service

Bardzo łatwo, poczuwszy smak do endpointów, zacząć „dłubać jeszcze trochę" – i nagle znaleźć się w projekcie na trzy miesiące. Żeby tak się nie stało, ważne jest utrwalenie, co dokładnie jest minimalnym web-baseline na tym etapie: jakie URL są już podniesione, jaką rolę pełnią i dlaczego tyle wystarczy, żeby uważać serwis za żywy. To moment dyscypliny: nie „niedorobiliśmy", świadomie się zatrzymaliśmy.

Utrwalmy bieżącą „mapę" serwisu w formie tabeli. Pomaga to i Tobie jako deweloperowi, i temu, kto będzie sprawdzał projekt (np. Twój przyszły współpracownik, który lubi „a gdzie lista endpointów?").

Endpoint Co zwraca Po co jest teraz
GET / index.html Szybkie wejście do serwisu przez przeglądarkę, nawigacja po API
GET /api/catalog/courses List<CourseCard> Podstawowa lista kursów read-only
GET /api/catalog/courses/{slug} 200 + CourseCard / 404 Pobranie jednej karty po kluczu i uczciwa odpowiedź, jeśli kursu nie ma
GET /api/catalog/featured List<CourseCard> Mała podborka „rekomendowanych"

Na razie wygląda to prawie jak zabawka, ale zwróć uwagę na wartość. Mamy jedno „ludzkie wejście" (landing page) i trzy proste wejścia JSON. To wystarczy, żeby zademonstrować, że Spring Boot podniósł web-runtime, że warstwa kontrolera działa, że model domenowy wyjeżdża na zewnątrz jako JSON i że serwis można badać w przeglądarce bez dodatkowych narzędzi.

Teraz połączmy to w jedną „historię kliknięcia". Użytkownik otwiera /, widzi linki, klika na „Wszystkie kursy", dostaje JSON. Tak, przeglądarka pokaże JSON niezbyt ładnie, ale za to ta ścieżka jest zrozumiała dla każdego. Oto diagram tego przepływu:

flowchart TD
    U["Użytkownik w przeglądarce"] --> A["GET /"]
    A --> S["Spring Boot"]
    S --> H["Static resource: index.html"]
    H --> U

    U --> B["Kliknięcie w link: /api/catalog/courses"]
    B --> S
    S --> D["DispatcherServlet"]
    D --> C["CourseCatalogController"]
    C --> L["CourseCatalogService"]
    L --> R["InMemoryCourseCatalogRepository"]
    R --> L --> C --> D --> U["Odpowiedź JSON"]

Jeśli rozumiesz ten schemat – nie „magicznie wywołujesz kontrolery", naprawdę widzisz architekturę: zasoby statyczne obsługiwane są jednym mechanizmem, endpointy JSON – innym, a dane biznesowe pozostają w serwisie/repozytorium.

6. Ręczne sprawdzenie serwisu

Gdy serwis dopiero się pojawia, najczęściej sprawdza się go „po biedzie": przeglądarką i curl. I to normalne. Na tym etapie nie potrzebujemy pięknego klienta API, potrzebujemy pewności: „podniósł się", „trasa jest", „odpowiedź przychodzi". Najważniejsze – nie zamieniać ręcznego sprawdzania w chaos „gdzieś widziałem ten URL…".

Najprostsze ręczne sprawdzenie zaczyna się od landing page:

# Sprawdzamy, że korzeń odpowiada HTML i że serwer w ogóle się podniósł
curl -i http://localhost:8080/

Zobaczysz status (najpewniej 200 OK) i HTML w treści odpowiedzi. W przeglądarce będzie to jeszcze przyjemniejsze, bo można po prostu klikać linki.

Dalej sprawdzamy endpointy JSON. Na przykład lista kursów:

# Sprawdzamy, że endpoint zwraca JSON (i że serializacja działa)
curl -i http://localhost:8080/api/catalog/courses

Jeśli wszystko gra, dostaniesz Content-Type: application/json i tablicę obiektów. Dla nowicjusza ważny moment: „tablica obiektów" nie oznacza, że gdzieś ręcznie sklejałeś JSON. Zwróciłeś List<CourseCard>, a Spring MVC + Jackson zrobiły resztę.

I od razu przydatne jest sprawdzenie negatywnego scenariusza – nieznany slug:

# Sprawdzamy zachowanie na nieistniejący slug: w finalnym baseline to uczciwy 404
curl -i http://localhost:8080/api/catalog/courses/i-do-not-exist

Oczekiwany wynik – 404 Not Found. To właśnie normalna odpowiedź dla zasobu, którego nie ma. Jeśli zamiast tego masz 500, to znaczy, że w kodzie zostało stare zachowanie z wyjątkiem, a nie kanoniczny wariant z ResponseEntity.notFound().

Ten zestaw sprawdzeń już wystarczy, żeby zobaczyć dwie różne role warstwy web: korzeń / oddaje ludzkie wejście do serwisu, a /api/catalog/... oddaje domenowe odpowiedzi JSON.

7. Granica: kurs Boot i projektowanie REST

Najtrudniejsza rzecz w projekcie dydaktycznym – nie pisać kod, lecz na czas przestać pisać kod. Szczególnie gdy wszystko zaczęło wychodzić. Dlatego teraz ważne jest utrwalenie granicy: landing page i kilka endpointów read-only – to web-baseline, a nie pełnoprawne projektowanie API. Potwierdzamy, że serwis stał się serwisem web, ale nie udajemy, że już zbudowaliśmy „idealne REST".

Czego celowo nie robimy na tym etapie? Nie dodajemy operacji zapisu (POST/PUT/PATCH/DELETE), bo to natychmiast prowadzi do dużych tematów: treść żądania, walidacja danych wejściowych, granice transakcji, błędy, spójność, bezpieczeństwo. Nie budujemy też warstwy DTO „na wszelki wypadek": w naszym dydaktycznym serwisie model read (CourseCard) jest na razie wystarczająco przejrzysty i dodatkowe opakowania tylko pogorszą czytelność.

Nie podłączamy silników szablonów (Thymeleaf, Freemarker) i nie budujemy HTML po stronie serwera. Jeśli chcesz „pięknej strony kursu po slug", to już osobny świat z warstwą view. Na tym etapie landing page – to nawigacja, a nie aplikacja UI. Nie projektujemy też jednolitego error-contract i nie dokumentujemy API przez OpenAPI. To ważne rzeczy, ale mają swój kurs i kontekst i wymagają już bardziej pewnego władania warstwą web.

W efekcie granica brzmi tak: w ramach kursu Spring Boot chcemy, żebyś pewnie podniósł aplikację, rozumiał jej wiring i web-baseline, potrafił oddawać JSON i czynić serwis obserwowalnym/zarządzalnym przez konfigurację (do tych tematów dojdziemy później). A zamienianie projektu w „prawie prod" REST API teraz nie będziemy. I to nie „niedorobienie" – to metodyczna czystość. Inaczej zamiast fundamentu dostaniemy niekończącą się budowę, gdzie fundament wciąż się wylewa od nowa.

8. Typowe błędy przy landing page i nawigacji

Gdy po raz pierwszy dodajesz landing page do serwisu Boot, błędy zazwyczaj są bardzo życiowe i dlatego szczególnie przykre: wszystko „prawie działa", ale korzeń daje 404, albo HTML leży „gdzieś obok", albo nagle zamiast strony przychodzi ciąg JSON. Poniżej – najczęstsze grabie i krótkie wyjaśnienie, dlaczego tak się dzieje.

Błąd nr 1: index.html wrzucono nie tam i Boot go nie widzi.
Często plik wrzuca się do src/main/java/... obok klas lub do korzenia projektu. Ale Spring Boot serwuje statykę z classpath, czyli z src/main/resources (i konkretnych standardowych podfolderów). Jeśli pliku nie ma w src/main/resources/static/index.html (lub innym standardowym miejscu), przy GET / dostaniesz 404 i będzie to „właściwy" 404: serwisu jakby nie ma. Leczy się bardzo prosto – właściwą ścieżką pliku i sprawdzeniem, że trafił do buildu.

Błąd nr 2: zrobiłeś kontroler na / i dziwisz się, że landing page zniknęła.
To normalne: jawny mapping w MVC zazwyczaj ma priorytet i statyczny zasób przestaje być używany jako welcome page. Szczególnie często zdarza się, gdy piszą @RestController i dodają @GetMapping("/") „do sprawdzenia". W efekcie przeglądarka dostaje ciąg (lub JSON), a Ty czekasz na HTML. Rozwiązanie proste: albo naprawdę chcesz kontroler na / (to już osobny design), albo zostawiasz / pod statyczną welcome page i nie ruszasz go kontrolerami.

Błąd nr 3: próba „renderowania HTML" z @RestController, zwracając ciągi.
@RestController semantycznie mówi Spring MVC: „wynik metody – to treść odpowiedzi". Dlatego ciąg staje się plain text (lub ciągiem JSON), a nie stroną HTML. Nowicjusz widzi <h1>...</h1> w przeglądarce i myśli „dlaczego nie wyrenderowało?". Bo nie jesteś w świecie szablonów i view-resolver, jesteś w świecie @ResponseBody. Jeśli potrzebny HTML bez szablonów – zasoby statyczne; jeśli dynamiczny HTML – to osobna gałąź technologiczna, której tutaj nie rozwijamy.

Błąd nr 4: dodano linki do endpointów, których jeszcze nie ma i samemu zafundowano sobie quest z 404.
Landing page jest przydatna, gdy jest uczciwa. Jeśli w index.html dodano dziesięć linków „na przyszłość", przeglądarka zamienia się w generator 404, a Ty – w osobę, która nie rozumie, co „się zepsuło". Lepiej trzymać stronę jak „żywą mapę": tylko naprawdę istniejące trasy bieżącego etapu i jeden przykład z slug, który na pewno istnieje w Twoich danych.

Błąd nr 5: logika pobierania danych trafiła do kontrolera i kontroler zaczął puchnąć.
To nie jest fatalne w kodzie dydaktycznym, ale to zły nawyk. Jak tylko kontroler zaczyna filtrować, sortować, wybierać featured lub szukać kursu po slug samodzielnie, przestaje być adapterem i staje się mini-serwisem, tylko przywiązanym do HTTP. Potem tę samą logikę chce się użyć gdzie indziej i zaczyna się kopiowanie. Prawidłowiej trzymać kontroler cienkim: wziąć parametry żądania, wywołać serwis, zwrócić wynik.

1
Zadanie
Spring Boot, poziom 10, lekcja 4
Niedostępne
Strona startowa z `static/index.html`
Strona startowa z `static/index.html`
1
Zadanie
Spring Boot, poziom 10, lekcja 4
Niedostępne
Strona lądowania i lekki endpoint metadanych
Strona lądowania i lekki endpoint metadanych
1
Ankieta/quiz
Spring MVC, poziom 10, lekcja 4
Niedostępny
Spring MVC
Podstawy webowe Spring Boot
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION