Realny cel: odpalić pierwsze REST API w Spring Boot, które zwraca JSON, przyjmuje JSON, waliduje dane i oddaje sensowne błędy. Przy okazji: podjąć trzy decyzje startowe bez pakowania się w „architekturę jak z NASA”.
Pytania, które zwykle blokują start (i na które padają konkretne odpowiedzi w kolejnych sekcjach):
- Jak najszybciej utworzyć projekt Spring Boot pod API i co wybrać w generatorze?
- Spring Web (MVC) czy WebFlux — kiedy to realnie ma znaczenie?
- Maven czy Gradle — co prostsze na start i jak to wpływa na codzienną pracę?
- Jak wygląda minimalny endpoint REST (GET/POST) i gdzie go umieścić w projekcie?
- Jak uruchomić aplikację lokalnie i jak ją najprościej przetestować (curl / HTTP client w IDE)?
- Jakie startery są naprawdę potrzebne, a jakie to „bloat” na pierwsze podejście?
- Jak dodać walidację DTO i jeden wspólny format błędów bez rozbudowy frameworka?
- Co sprawdzić, gdy aplikacja nie wstaje albo nagle dostajesz pierwsze 500?
Trzy decyzje na starcie (i które możesz odłożyć bez wyrzutów)
Decyzje, które faktycznie wpływają na pierwsze godziny
Na początku liczą się trzy wybory, bo determinują, czy po 15 minutach masz działające API, czy po godzinie nadal walczysz z konfiguracją:
- sposób utworzenia projektu (Spring Initializr vs generator w IDE vs ręczne składanie),
- narzędzie budowania (Maven vs Gradle),
- stos webowy (Spring Web MVC vs WebFlux).
Reszta to dodatki. Przy pierwszym API lepiej wyrobić nawyk „działa, rozumiem, dopiero rozbudowuję” niż „mam 40 zależności i nie wiem, która robi co”.
Decyzje, które kuszą, ale są szumem na start
Na listę „za wcześnie” często wpadają: baza danych (JPA), Spring Security, mikroserwisy, Kafka, Docker, wzorce warstwowe w pięciu pakietach i repozytorium na wszystko. To są ważne tematy — po prostu nie są konieczne, żeby postawić pierwsze sensowne API.
Minimalny, sensowny cel na szybki start API w Spring Boot to:
- aplikacja się uruchamia,
- są dwa endpointy (GET i POST),
- POST przyjmuje JSON i ma walidację,
- błędy walidacji wracają jako 400 w przewidywalnym formacie.
Dlaczego „pierwsze 500” przychodzi szybciej niż kawa
Gdy robisz pierwszy kontroler, bardzo łatwo przypadkiem wywołać wyjątek (np. null, zła walidacja, brak zależności). To normalne. Różnica między przyjemnym startem a frustracją polega na tym, czy umiesz szybko odróżnić: błąd klienta (400) od błędu serwera (500) i czy widzisz w odpowiedzi coś więcej niż „Internal Server Error”. Ten tekst prowadzi do stanu, w którym 500 zdarza się rzadziej i przestaje być tajemniczą magią.
Warianty utworzenia projektu: Initializr vs generator w IDE vs ręczne składanie zależności
Spring Initializr (web) — domyślny wybór dla większości
Spring Initializr to najprostsza droga, bo dostajesz projekt ze spójnymi wersjami zależności (Boot ogarnia BOM) i sensowną strukturą. Największy plus dla początkujących: mniejsza szansa, że wylądujesz z konfliktem wersji zanim napiszesz pierwszą linijkę kodu.
Co wybrać w metryczce, żeby nie strzelić sobie w stopę
- Java: wybierz wersję LTS wspieraną w Twoim środowisku (najczęściej 17 lub 21). Jeśli nie wiesz — 17 to bezpieczny punkt startu.
- Packaging: Jar (na start niemal zawsze).
- Group i Artifact: użyj sensownych nazw, bo to wpłynie na pakiet bazowy i identyfikację projektu.
- Package name: trzymaj porządek od początku (np. pl.twojadomena.demo), bo później będzie to „root” skanowania komponentów.
Minimalne startery pod REST API
Na szybki start API zwykle wystarczą:
- Spring Web (czyli spring-boot-starter-web) — MVC, kontrolery, wbudowany Jackson do JSON.
- Validation (czyli spring-boot-starter-validation) — walidacja adnotacjami + integracja z kontrolerami.
Opcjonalnie (ale nie obowiązkowo):
- Lombok — skraca kod, ale utrudnia zrozumienie, co naprawdę dzieje się w klasie, jeśli dopiero uczysz się Javy + Springa.
- Actuator — super do diagnostyki, ale to raczej „krok 2”, gdy już masz API i chcesz więcej wglądu.
Generator w IDE (IntelliJ / Eclipse) — kiedy ma sens
Jeśli i tak pracujesz w IDE, generator projektu Spring Boot bywa wygodny: mniej przełączania kart, IDE od razu indeksuje projekt, tworzy konfiguracje uruchomieniowe. To jest szczególnie przyjemne, gdy dopiero oswajasz się z ekosystemem Springa.
Pułapka: w generatorach IDE łatwo „doklikać” 10 zależności, bo brzmią znajomo (Security! JPA! Cloud!). Tylko że po tygodniu trudno pamiętać, po co to w ogóle było. Trzymaj się minimalnego zestawu i dodawaj kolejne elementy dopiero, gdy umiesz uzasadnić: co to daje i jak to przetestuję.
Ręczne dopisywanie zależności — dlaczego to antywzorzec dla początkujących
Ręczne budowanie projektu „od zera” ma sens, gdy już wiesz, jak działa zarządzanie wersjami zależności w Spring Boot i co oznaczają konflikty transitive dependencies. Na starcie to zwykle przepis na problemy typu:
- aplikacja nie wstaje, bo biblioteki się gryzą,
- brakuje Jacksona albo walidacji, choć „przecież dodałem coś od weba”,
- pojawia się błąd klasy w runtime, bo wersje nie pasują do Boota.
Jeśli celem jest szybki start REST API w Spring Boot, Initializr wygrywa prostotą i przewidywalnością.
Porównanie wariantów startu projektu
| Wariant | Najlepszy na start, bo… | Pułapki | Kiedy nie wybierać |
|---|---|---|---|
| Spring Initializr (web) | Powtarzalny szkielet, spójne wersje, szybkie dodanie starterów | Można dodać za dużo zależności „bo są” | Gdy masz sztywny firmowy szablon repo lub monorepo z narzuconą strukturą |
| Generator w IDE | Wygoda, IDE od razu konfiguruje uruchamianie i indeksowanie | „Przeklikanie” wielu modułów bez zrozumienia skutków | Gdy IDE jest ograniczone albo zespół bazuje na skryptach i CI, a nie na konfiguracjach IDE |
| Ręczne składanie zależności | Daje kontrolę, gdy wiesz co robisz | Konflikty wersji, dłuższa diagnostyka, łatwe pominięcie kluczowych elementów | Gdy dopiero zaczynasz i chcesz po prostu postawić pierwsze API |
Maven vs Gradle: wybór narzędzia budowania bez „wielkiej wojny build tooli”
Kryteria praktyczne dla początkującego
To nie jest wybór „lepszy/gorszy”, tylko „co szybciej ogarnę i co będzie pasować do kontekstu”. Dobre kryteria na start:
- standard w zespole — jeśli projekt ma trafić do zespołu, ich wybór jest ważniejszy niż Twoje preferencje,
- czytelność konfiguracji — czy łatwo znajdziesz, gdzie zmienić wersję Javy, nazwę artefaktu, zależności,
- wsparcie w IDE i CI — oba narzędzia są wspierane, ale w praktyce liczy się to, co już masz „gotowe” w środowisku,
- krzywa uczenia — ile nowej składni musisz poznać, zanim ruszysz dalej.
Kiedy Maven jest rozsądnym wyborem
Maven bywa bardziej „oczywisty” na start, bo pom.xml jest deklaratywny i łatwo znaleźć przykłady. Jeśli tworzysz projekt rekrutacyjny, uczysz się Spring Boot od zera albo chcesz iść najbardziej standardową ścieżką — Maven jest bezpieczny.
Typowe komendy:
- build: mvn clean package
- test: mvn test
- run: mvn spring-boot:run
Kiedy Gradle ma więcej sensu
Gradle często wygrywa, jeśli zespół już go używa, projekt będzie wielomodułowy albo chcesz większej elastyczności. Tylko pamiętaj: elastyczność oznacza też więcej możliwości zrobienia sobie krzywdy w buildzie, gdy dopiero się uczysz.
Typowe komendy (wrapper):
- build: ./gradlew build
- test: ./gradlew test
- run: ./gradlew bootRun
Najczęstszy błąd: mieszanie porad z Maven i Gradle
Brzmi banalnie, ale to klasyk: ktoś kopiuje fragment z poradnika o Mavenie do projektu w Gradle albo odwrotnie, a potem dziwi się, że „nie działa”. Pierwszy krok diagnostyczny, gdy coś jest nie tak: upewnij się, czy projekt jest Mavenowy (pom.xml) czy Gradlowy (build.gradle/build.gradle.kts) i trzymasz się jednego świata naraz.
Spring Web (MVC) vs WebFlux: sensowne kryteria, nie religia
Spring MVC (spring-boot-starter-web) — domyślnie dla REST i CRUD
Dla większości początkujących Spring Web MVC to najlepszy start: klasyczny model request/response, łatwe debugowanie, proste kontrolery, prosty mental model. Jeśli robisz typowe REST API, integrację z innym systemem, małe API rekrutacyjne albo ćwiczeniowe — MVC jest naturalne.
W MVC bardzo łatwo zrozumieć, co się dzieje:
- przychodzi żądanie HTTP,
- kontroler je mapuje,
- zwracasz obiekt,
- Spring + Jackson zamienia go na JSON.
WebFlux — kiedy naprawdę ma sens
WebFlux jest sensowny, gdy masz konkretne wymagania: streaming danych, SSE/WebSocket, backpressure, duże obciążenie I/O i reaktywne integracje end-to-end (np. cały łańcuch jest reaktywny, a nie tylko kontroler).
Koszt wejścia jest realny: inny styl myślenia, inne typy (Mono/Flux), inne pułapki. Najczęstsza: w aplikacji reaktywnej wstawiasz blokujące wywołania (np. klasyczne JDBC) i nagle tracisz korzyści, a zyskujesz trudniejszą diagnostykę. WebFlux bez potrzeby bywa jak kupno kombajnu do krojenia chleba — robi wrażenie, ale kromka i tak jest jedna.
Proste kryterium wyboru (bez długich debat)
- Jeśli nie masz pewności, wybierz Spring Web MVC.
- Jeśli wiesz, że potrzebujesz streamingu / SSE / WebSocket lub reaktywnego łańcucha I/O — rozważ WebFlux.
- Jeśli Twoje API to klasyczne CRUD — WebFlux raczej skomplikuje start.
Minimalny „Hello API”: 2 endpointy, JSON i kody HTTP bez rozbudowanej architektury
Minimalna struktura pakietów, która nie boli
Na szybki start API nie potrzebujesz dziesięciu warstw. Wystarczy czytelny podział, który rośnie razem z projektem. Przykład:
- pl.twojadomena.api (pakiet bazowy, tu jest klasa z main)
- pl.twojadomena.api.controller
- pl.twojadomena.api.dto
- pl.twojadomena.api.error (opcjonalnie na obsługę błędów)
Warstwy typu service/repository wprowadzaj, gdy pojawia się logika, którą chcesz testować i utrzymywać. Jeśli kontroler ma 20 linijek i robi prostą transformację — nie ma sensu udawać wielkiego systemu.
Endpoint GET: szybki sanity check, że aplikacja żyje
Najprostszy punkt kontrolny to endpoint, który zwraca JSON. Dzięki temu od razu wiesz, że działa routing, kontroler i serializacja.
Najprościej: zwróć mapę albo mały DTO. Spring dorzuci nagłówki i kod 200, a Jackson zrobi z obiektu JSON bez proszenia się o pozwolenie.
@RestController
class HelloController {
@GetMapping("/api/hello")
Map<String, Object> hello() {
return Map.of("message", "Hello API", "status", "ok");
}
}
Jeśli po odpaleniu aplikacji wejdziesz na /api/hello i widzisz JSON, masz potwierdzone trzy rzeczy naraz: działa routing, działa kontroler, działa serializacja. W praktyce to najlepszy „bezpiecznik” przed grzebaniem godzinę w zależnościach, gdy problem był banalny (np. zły port).
Endpoint POST: przyjmij JSON, zwróć 201 i lokalizację zasobu
Drugi endpoint powinien dotknąć tego, co w API boli najbardziej: parsowanie wejścia, walidacja i sensowne kody HTTP. Weźmy prosty przykład tworzenia „notatki” (bez bazy, na skróty — magazyn w pamięci wystarczy do nauki).
record CreateNoteRequest(String title, String content) {}
record NoteResponse(Long id, String title, String content) {}
@RestController
@RequestMapping("/api/notes")
class NotesController {
private final AtomicLong seq = new AtomicLong();
private final Map<Long, NoteResponse> store = new ConcurrentHashMap<>();
@PostMapping
ResponseEntity<NoteResponse> create(@RequestBody CreateNoteRequest req) {
long id = seq.incrementAndGet();
var saved = new NoteResponse(id, req.title(), req.content());
store.put(id, saved);
return ResponseEntity
.created(URI.create("/api/notes/" + id))
.body(saved);
}
@GetMapping("/{id}")
ResponseEntity<NoteResponse> get(@PathVariable long id) {
var note = store.get(id);
return (note != null) ? ResponseEntity.ok(note) : ResponseEntity.notFound().build();
}
}
To jest celowo „prostackie”, ale uczy dobrych odruchów: POST zwraca 201 Created i nagłówek Location, a GET umie oddać 404, zamiast magicznego null. Taki zestaw szybko wychwytuje typowe pomyłki: brak Content-Type: application/json, literówki w polach JSON, mylenie ścieżki (/api/note vs /api/notes) — czyli „klasyka weekendowego API”.
DTO + walidacja wejścia: najmniej kodu, największy zysk
DTO jako kontrakt: nie wystawiaj encji i nie „bierz Mapy”
DTO to prosty filtr między światem HTTP a resztą aplikacji. Dzięki temu nie wiążesz się przypadkiem ze strukturą domeny ani z tym, jak trzymasz dane. W praktyce różnica jest ogromna: możesz zmienić pole w środku aplikacji, a kontrakt API zostaje stabilny. I odwrotnie — możesz dodać pole do odpowiedzi, nie psując zapisu w bazie.
Unikaj dwóch skrajności: przyjmowania Map<String, Object> (bo wtedy walidacja i błędy są jak wróżenie z fusów) oraz wystawiania encji JPA „bo już jest”. DTO w formie record jest lekkie, czytelne i wystarczające do większości endpointów.
Walidacja Bean Validation: jasne błędy zamiast „500 i cisza”
Żeby walidacja zadziałała, dorzuć starter: spring-boot-starter-validation. Potem adnotacje w DTO i jedno @Valid w kontrolerze. Tyle. Żadnych ręcznych ifów na początku metody, które po miesiącu wyglądają jak regulamin osiedla.
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
record CreateNoteRequest(
@NotBlank @Size(max = 100) String title,
@Size(max = 2000) String content
) {}
@PostMapping
ResponseEntity<NoteResponse> create(@Valid @RequestBody CreateNoteRequest req) {
// ...
}
Efekt: jeśli ktoś wyśle pusty title, dostanie 400 Bad Request z opisem błędu walidacji, a nie „Internal Server Error”, po którym jedyną wskazówką jest stacktrace.
Jak wygląda odpowiedź walidacji i jak ją szybko „ucywilizować”
Domyślna odpowiedź przy błędach walidacji potrafi być poprawna technicznie, ale średnio przyjazna dla człowieka (albo dla frontendu, który chce listę pól i komunikatów). Dwa warianty są najczęstsze:
- Wariant A: zostawiasz domyślne zachowanie — szybkie, OK do nauki i prostych projektów.
- Wariant B: dodajesz własny format błędu — minimalnie więcej kodu, ale od razu lepsza ergonomia API.
Obsługa błędów: domyślnie vs własne @RestControllerAdvice
Wariant 1: domyślne błędy Springa (na start: akceptowalne)
Spring Boot zwróci sensowny status (np. 400 dla walidacji, 404 dla brakującego endpointu), a w treści pojawi się opis błędu. Jeśli Twoim celem jest tylko „żeby działało” — możesz na tym poprzestać.
Minus jest praktyczny: format odpowiedzi nie jest „kontraktem”, tylko implementacją. Raz jest bardziej opisowy, raz mniej (zależy od wersji, ustawień, a czasem od tego, czy błąd był z walidacji czy z parsowania JSON).
Wariant 2: jedno miejsce na błędy w API (minimum, które robi różnicę)
Najprostszy sposób, żeby API zachowywało się przewidywalnie: @RestControllerAdvice. Złapiesz typowe wyjątki i zwrócisz jednolity JSON. Bez frameworków do frameworków.
package pl.twojadomena.api.error;
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.LinkedHashMap;
import java.util.Map;
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> fields = new LinkedHashMap<>();
for (FieldError fe : ex.getBindingResult().getFieldErrors()) {
// przy duplikatach zostawiamy pierwszy komunikat, żeby nie robić śmietnika
fields.putIfAbsent(fe.getField(), fe.getDefaultMessage());
}
var body = Map.<String, Object>of(
"error", "validation_failed",
"fields", fields
);
return ResponseEntity.badRequest().body(body);
}
@ExceptionHandler(HttpMessageNotReadableException.class)
ResponseEntity<Map<String, Object>> handleBadJson(HttpMessageNotReadableException ex) {
var body = Map.<String, Object>of(
"error", "invalid_json",
"message", "Nie da się sparsować JSON-a. Sprawdź składnię i typy pól."
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
}
@ExceptionHandler(Exception.class)
ResponseEntity<Map<String, Object>> handleUnexpected(Exception ex) {
// Tu nie wylewaj stacktrace do klienta. Logi są dla Ciebie.
var body = Map.<String, Object>of(
"error", "internal_error",
"message", "Coś poszło nie tak po stronie serwera."
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
}
}
Plusy/minusy tego wariantu są bardzo „życiowe”:
- Plus: klient zawsze dostaje przewidywalny JSON; łatwiej to obsłużyć w UI i testach.
- Plus: masz jedno miejsce, żeby dopiąć sensowne komunikaty.
- Minus: da się przesadzić i zrobić z tego mini-platformę do obsługi wszystkich możliwych wyjątków. Na start nie musisz.
Kiedy który wariant ma sens
- Domyślne błędy — nauka, krótkie demo, jednorazowy endpoint do integracji, gdzie klientem jesteś tylko Ty (np. curl).
- @RestControllerAdvice — cokolwiek, co ma żyć dłużej niż weekend, mieć front albo być używane przez kogoś poza Tobą. Tak, nawet w projekcie rekrutacyjnym wygląda to dojrzale i nie wymaga tony kodu.
Szybkie testowanie endpointów: curl vs klient HTTP w IDE
Wariant 1: curl — najszybsze, gdy chcesz „widzieć HTTP”
Curl daje brutalnie prawdziwy obraz: nagłówki, statusy, body. Idealne do sanity checku, gdy coś nie gra z Content-Type albo kodem odpowiedzi.
curl -i http://localhost:8080/api/hellocurl -i -X POST http://localhost:8080/api/notes
-H "Content-Type: application/json"
-d '{"title":"Pierwsza notatka","content":"Test"}'Jeśli dostaniesz 415 Unsupported Media Type, to niemal zawsze brakuje Content-Type: application/json. Jeśli 400 i komunikat o parsowaniu — JSON jest popsuty (czasem winny bywa cudzysłów „inteligentny” skopiowany z komunikatora, bo technologia lubi żarty).
Wariant 2: HTTP Client w IDE — wygodniej do powtarzania i trzymania requestów w repo
Jeśli używasz IntelliJ, plik .http (albo odpowiednik w innym IDE) pozwala trzymać gotowe requesty obok kodu. To praktycznie takie „manualne testy”, które nie znikają po zamknięciu terminala.
### Hello
GET http://localhost:8080/api/hello
### Create note
POST http://localhost:8080/api/notes
Content-Type: application/json
{
"title": "Notatka",
"content": "Treść"
}
### Get note
GET http://localhost:8080/api/notes/1
Wybór jest prosty:
- curl — gdy debugujesz protokół i chcesz pełną kontrolę.
- HTTP Client w IDE — gdy chcesz szybko klikać, iterować i mieć zestaw requestów „pod ręką”.
Najczęstsze powody, że aplikacja nie wstaje (i jak nie spędzić tu wieczoru)
Port zajęty: klasyczne „Address already in use”
Gdy w logach widzisz, że 8080 jest zajęty, masz trzy sensowne wyjścia: ubij proces, zmień port albo odpal na innym profilu/konfiguracji. Najszybciej zmienić port w application.properties:
server.port=8081Nie ta wersja Javy
Objawy są różne: od komunikatów o „unsupported class file major version” po dziwne błędy pluginów buildu. Sprawdź:
- jaką Javą budujesz: mvn -v / ./gradlew -v,
- jaką Javą uruchamiasz w IDE (Project SDK),
- jaką wersję ustawiono w projekcie (np. w Maven/Gradle).
Najczęściej problemem jest miks: kompilujesz jedną Javą, uruchamiasz drugą.
Brak adnotacji / zły pakiet bazowy (czyli Spring „nie widzi” kontrolera)
Jeśli endpoint zwraca 404, a aplikacja działa, to często kontroler nie został zeskanowany. Dwa szybkie testy:
- czy klasa aplikacji z
public static void mainjest w pakiecie nadrzędnym (np.pl.twojadomena.api), - czy kontroler ma
@RestControlleri poprawne mapowania.
Spring skanuje komponenty „w dół” od pakietu klasy głównej. Gdy kontroler wyląduje przypadkiem w innym korzeniu pakietów, będzie jak plakat w piwnicy — istnieje, ale nikt go nie widział.
Startery „na krzyż”: MVC i WebFlux albo brak walidacji
Na start trzymaj zależności prosto:
- dla MVC: spring-boot-starter-web,
- dla WebFlux: spring-boot-starter-webflux (bez web, chyba że wiesz, po co mieszasz),
- dla walidacji: spring-boot-starter-validation.
Najbardziej zdradliwy błąd początkujących: kopiujesz DTO z adnotacjami walidacyjnymi, dodajesz @Valid, a walidacja „nie działa” — bo brakuje startera validation. Wtedy aplikacja zwykle wystartuje, tylko Twoje API zacznie przyjmować rzeczy, których nie powinno. I to jest gorsze niż crash, bo psuje dane po cichu.
Minimalna struktura projektu: „jeden kontroler” vs lekkie warstwy
Na starcie są dwie sensowne szkoły. Obie działają, ale każda ma inne koszty, gdy projekt zaczyna rosnąć (a rośnie szybciej niż lista TODO).
Wariant 1: wszystko w jednym kontrolerze (najszybszy start)
Masz 2–3 endpointy, proste DTO i koniec. Trzymasz logikę blisko HTTP, żeby szybciej zobaczyć efekt. To jest OK, dopóki nie zaczynasz przepisywać tych samych fragmentów w pięciu metodach.
- Plus: najmniej plików, najmniej skakania po projekcie.
- Plus: najszybciej „coś działa” i od razu testujesz HTTP.
- Minus: kontroler szybko puchnie (i nie, nie od Springa, tylko od wymagań).
- Minus: trudniej później wydzielać logikę bez dużego refactoru.
Dla kogo: demo, nauka, małe API do integracji, zadanie rekrutacyjne z krótkim zakresem.
Wariant 2: kontroler + serwis (minimum porządku bez „enterprise”)
Najprostszy kompromis: kontroler robi HTTP (mapowanie, kody), serwis robi logikę. Nadal bez repozytoriów, baz i architektonicznych przemówień motywacyjnych.
package pl.twojadomena.api.notes;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Service
class NoteService {
private final AtomicLong seq = new AtomicLong(0);
private final Map<Long, NoteDto> store = new ConcurrentHashMap<>();
NoteDto create(NoteCreateRequest req) {
long id = seq.incrementAndGet();
var dto = new NoteDto(id, req.title(), req.content());
store.put(id, dto);
return dto;
}
Optional<NoteDto> get(long id) {
return Optional.ofNullable(store.get(id));
}
}
Kontroler robi się wtedy czytelny i przewidywalny:
package pl.twojadomena.api.notes;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/notes")
class NoteController {
private final NoteService service;
NoteController(NoteService service) {
this.service = service;
}
@PostMapping
ResponseEntity<NoteDto> create(@Valid @RequestBody NoteCreateRequest req) {
var created = service.create(req);
return ResponseEntity.status(201).body(created);
}
@GetMapping("/{id}")
ResponseEntity<NoteDto> get(@PathVariable long id) {
return service.get(id)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
}
- Plus: łatwiej testować logikę poza HTTP (serwis bez MockMvc).
- Plus: gdy dojdzie baza lub integracja, masz gdzie to włożyć bez „przepychania” przez kontroler.
- Minus: więcej plików i więcej połączeń między klasami (ale nadal w granicach zdrowego rozsądku).
Dla kogo: wszystko, co ma szansę dostać 5+ endpointów lub drugą osobę w zespole.
Proste kryteria wyboru (bez filozofii)
- Jeśli po jednym dniu kodowania kontroler ma więcej niż ~150–200 linii i zaczynasz przewijać — rozdziel na serwis.
- Jeśli logika zaczyna się powtarzać (np. walidacje „biznesowe”, mapowanie, formatowanie) — rozdziel na serwis.
- Jeśli to czysty „Hello API” — jeden kontroler jest w porządku.
Maven czy Gradle w praktyce: co wybierzesz, gdy chcesz po prostu pisać API
Da się żyć z jednym i drugim. Różnica dla początkującego zwykle nie jest w „wydajności buildu”, tylko w ergonomii i w tym, jak szybko ogarniesz konfigurację bez wywracania się na składni.
Wariant 1: Maven (bardziej „opisowy”, mniej zaskoczeń)
Maven ma pom.xml, który jest gadatliwy, ale przewidywalny. W większości tutoriali i projektów firmowych znajdziesz właśnie Maven, więc łatwiej porównać, skopiować kawałek konfiguracji i zrozumieć, co się dzieje.
- Plus: standardy i konwencje — mniej decyzji, mniej magii.
- Plus: łatwiej czytać na początku (XML jest nudny, ale stabilny).
- Minus: gdy zechcesz bardziej „programować build”, robi się ciężko.
Dla kogo: start, nauka Spring Boot, projekty rekrutacyjne, gdy chcesz minimalizować liczbę nowych rzeczy naraz.
Wariant 2: Gradle (bardziej elastyczny, ale łatwiej o „co tu się dzieje”)
Gradle (szczególnie w wersji Kotlin DSL) bywa przyjemny, bo konfiguracja jest krótsza i bliższa kodowi. Jednocześnie, jeśli nie znasz składni, potrafi spowolnić start — głównie przez to, że debugujesz build zamiast API.
- Plus: elastyczność i czytelne deklarowanie zależności (zwłaszcza w Kotlin DSL).
- Plus: w większych projektach łatwiej ogarniać wspólne ustawienia.
- Minus: dodatkowa krzywa uczenia, jeśli równolegle uczysz się Springa.
Dla kogo: gdy już miałeś kontakt z Gradle albo wiesz, że docelowo będziesz w nim siedzieć w pracy.
Tabela: szybkie porównanie (bez wojny religijnej)
| Kryterium | Maven | Gradle |
|---|---|---|
| Start dla początkującego | zwykle łatwiejszy | zależy od znajomości DSL |
| Czytelność „co jest zależnością, co pluginem” | klarownie (kosztem długości) | krócej, ale więcej „kodu” |
| Najczęstszy przypadek w tutorialach | częściej | też często, ale mniej „jednolicie” |
Spring Web (MVC) vs WebFlux: wybór na start bez bólu głowy
Tu najłatwiej się wykoleić, bo oba podejścia brzmią „nowocześnie”. Klucz: czy potrzebujesz reaktywności dzisiaj, czy tylko chcesz wystawić API i nie robić sobie pod górkę.
Wariant 1: Spring Web (MVC) — domyślny wybór dla większości API
MVC jest świetne, gdy Twoje API jest „klasyczne”: kontroler, serwis, wywołanie HTTP/DB, odpowiedź. Do większości CRUD-ów, integracji i projektów rekrutacyjnych to strzał w dziesiątkę.
- Plus: prostszy mental model (request wchodzi, odpowiedź wychodzi).
- Plus: ogrom przykładów i gotowców, mniej miejsc, gdzie można się zaskoczyć.
- Minus: przy bardzo dużej liczbie równoległych, wolnych requestów potrzebujesz skalować inaczej (zwykle poziomo).
Dla kogo: „pierwsze API”, większość usług REST, integracje systemów, panel admina, małe i średnie serwisy.
Wariant 2: WebFlux — gdy wiesz, po co go bierzesz
WebFlux ma sens, gdy świadomie budujesz coś reaktywnego: streaming, SSE, dużo połączeń czekających na I/O, albo cały ekosystem jest już w WebFlux (i nie chcesz mieszać paradygmatów).
- Plus: świetny do strumieniowania i pracy z „długimi” połączeniami.
- Plus: spójny model reaktywny, jeśli reszta stosu też jest reaktywna.
- Minus: więcej pojęć naraz (Publisher/Mono/Flux), trudniejsze debugowanie na starcie.
- Minus: mieszanie z blokującymi bibliotekami daje efekty w stylu „działa, ale czemu tak wolno?”.
Dla kogo: gdy masz realny wymóg (streaming, SSE, bardzo duża współbieżność) albo projekt i tak jest reaktywny.
Rekomendacja wyboru, jeśli nie masz twardych wymagań
- Jeśli nie wiesz — wybierz Spring Web (MVC).
- Jeśli musisz pytać, czy potrzebujesz WebFlux — zwykle nie potrzebujesz (jeszcze).
Najczęstszy błąd na finiszu: „dorzućmy wszystko, może się przyda”
Najłatwiej spalić szybki start, dorzucając od razu bazę, security, dokumentację, mappery, a potem przez godzinę walczyć z błędem, który jest tylko skutkiem ubocznym „jednej małej zależności”.
Minimalny, zdrowy zestaw na pierwsze API:
- spring-boot-starter-web (albo webflux, jeśli to świadoma decyzja),
- spring-boot-starter-validation,
- opcjonalnie: spring-boot-starter-test (domyślnie i tak jest w projektach z Initializr).
Gdy coś „nie działa”, najpierw sprawdź, czy to nie jest efekt uboczny dodatkowego startera. Spring jest szybki, ale nie czyta w myślach — a stos zależności potrafi mieć własną osobowość.
Najważniejsze punkty
- Na start liczą się tylko trzy decyzje: jak tworzysz projekt (Initializr/IDE/ręcznie), czym budujesz (Maven/Gradle) i czy idziesz w Spring Web MVC czy WebFlux — reszta może poczekać bez wyrzutów sumienia.
- Minimalny sensowny cel pierwszego API: aplikacja wstaje, masz GET i POST, POST przyjmuje JSON z walidacją, a błędy walidacji wracają jako czytelne 400 (nie tajemnicze 500).
- Spring Initializr to najbezpieczniejszy wybór dla większości: dostajesz spójne wersje zależności i przewidywalny szkielet, więc rzadziej utkniesz na konfliktach zanim napiszesz kontroler.
- Ustawienia z generatora mają znaczenie: Java LTS (17 jako bezpieczny start), packaging JAR oraz sensowne group/artifact/package name (np. pl.twojadomena.demo), bo to wpływa na strukturę i skanowanie komponentów.
- Do REST API zwykle wystarczą dwa startery: spring-boot-starter-web (kontrolery + JSON/Jackson) oraz spring-boot-starter-validation (adnotacje walidacyjne + integracja z endpointami).
- Generator w IDE jest wygodny, ale łatwo tam „doklikać” pół internetu (Security, JPA, Cloud…), a potem zgadywać, co właściwie psuje build — dodawaj zależności dopiero, gdy umiesz powiedzieć: po co i jak to przetestujesz.
- Ręczne składanie zależności na początku to proszenie się o klasyczne „nie wstaje, bo biblioteki się gryzą” (brak Jacksona, rozjechane wersje, błędy w runtime) — szybki start ma być szybki, nie jak budowa rakiety.






