Scenka startowa: od „czystej” Javy do pierwszego endpointu
Wyobraź sobie juniora, który spokojnie radzi sobie z kolekcjami, klasami i interfejsami w Javie, ale gdy ktoś rzuca hasło „Spring Boot”, widzi tylko gąszcz adnotacji i plików konfiguracyjnych. Odpala przykład z internetu, coś działa, ale nie ma pojęcia, dlaczego. Kiedy trzeba zmienić najmniejszy fragment kodu, wszystko się sypie – a w logach pojawia się ściana komunikatów, z których niewiele wynika.
Tak wygląda typowe wejście w Spring Boot bez zrozumienia podstaw. Z drugiej strony istnieje świat „starej szkoły” – ręczne stawianie Tomcata, serwlety, pliki web.xml, mozolne konfigurowanie wszystkiego samodzielnie. Spring Boot obiecuje: „jedno kliknięcie i masz gotową, działającą aplikację webową”. Działa to zaskakująco dobrze, ale tylko wtedy, gdy wiesz, co jest „pod spodem” i co robi za ciebie framework.
Cel jest prosty: od zera dojść do działającej aplikacji REST w Spring Boot – zrozumieć minimalny zestaw pojęć, zbudować kontroler, warstwę serwisu i prostą „pamięć” danych, a przy tym nie utknąć na typowych pułapkach początkujących. Kluczem jest połączenie podstaw Javy, podstaw HTTP i kilku kluczowych adnotacji Spring Boot w jedną, spójną całość.
Punkt startowy jest jasny: wystarczy solidne fundamenty z Javy, podstawowe zrozumienie, czym jest HTTP i chęć chwilowego zwolnienia tempa, żeby zamiast ślepego kopiowania tutoriali, świadomie przejść przez pierwszą aplikację REST.
Niezbędne fundamenty: co trzeba umieć z Javy, zanim dotkniesz Spring Boot
Absolutne minimum z Javy pod Spring Boot
Spring Boot nie jest frameworkiem do nauki samego języka. On zakłada, że Java jest już w miarę oswojona, przynajmniej na poziomie podstawowym. Zanim wejdziesz w adnotacje i endpointy REST, przyda się swoboda w takich obszarach jak:
- Klasy i obiekty – pola, konstruktory, metody, modyfikatory dostępu.
- Interfejsy – definiowanie kontraktów, implementacje, „programowanie do interfejsu”.
- Kolekcje – List, Set, Map, podstawowe operacje na nich.
- Wyjątki – różnica między checked i unchecked, try/catch, własne wyjątki.
- Java 8+ – lambdy, streamy na poziomie podstawowym, Optional, podstawy dat (java.time).
W codziennej pracy z Spring Boot bardzo szybko natkniesz się na listy obiektów, mapy konfiguracji, proste przetwarzanie strumieniowe oraz obsługę wyjątków. Bez tych elementów każda metoda serwisu będzie zagadką, a stacktrace z błędem przy pierwszej próbie wywołania endpointu może skutecznie zniechęcić.
OOP i zależności między klasami a DI w Spring
Spring Boot opiera się na idei wstrzykiwania zależności (Dependency Injection, DI). W czystej Javie zależność tworzysz ręcznie:
public class OrderService {
private final PaymentService paymentService = new PaymentService();
}W Springu ten kod jest rzadko spotykany. Zamiast tego deklarujesz, że klasa potrzebuje innego komponentu, a framework sam dostarcza odpowiednią instancję:
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}To działa tylko wtedy, gdy rozumiesz już koncept zależności między klasami: która klasa czego potrzebuje, gdzie powinna być logika biznesowa, a gdzie tylko delegowanie. Dependency Injection nie zastępuje myślenia o architekturze – ono tylko automatyzuje tworzenie i zarządzanie obiektami.
Konsola vs aplikacja webowa – co się zmienia
W aplikacji konsolowej cały przepływ kontrolujesz sam: od uruchomienia metody main do ostatniej linijki kodu. Program kończy się, gdy kończy się metoda main. W aplikacji webowej jest inaczej – startujesz serwer, który „nasłuchuje” żądań HTTP i reaguje na nie w momencie, gdy nadejdą.
To oznacza, że:
- nie ma jednego prostego „flow” programu – zamiast tego są requesty i odpowiedzi,
- część aplikacji odpowiada za przyjmowanie żądań (kontrolery),
- część za logikę (serwisy),
- a część za dane (repozytoria, baza danych).
Spring Boot uruchamia w środku wbudowany serwer (np. Tomcat), który nasłuchuje na porcie (np. 8080) i wywołuje odpowiednie metody kontrolerów na podstawie adresu URL i metody HTTP.
HTTP i REST – krótki fundament, bez którego się nie ruszysz
REST to nie jest magia Spring Boot. To po prostu styl budowania API w oparciu o protokół HTTP. W praktyce wystarczy kilka kluczowych pojęć:
- Metody HTTP – GET (pobieranie), POST (tworzenie), PUT/PATCH (aktualizacja), DELETE (usuwanie).
- URL – ścieżki, np.
/api/users,/api/users/1. - Statusy – 200 (OK), 201 (Created), 400 (Bad Request), 404 (Not Found), 500 (Internal Server Error).
- Body – dane przesyłane najczęściej w JSON.
Znając te podstawy, kontrolery REST w Spring Boot stają się dużo bardziej intuicyjne: widzisz @GetMapping i od razu rozumiesz, że ktoś będzie coś pobierał, @PostMapping – że tworzył, a kod statusu 404 nie jest już tajemniczym błędem, tylko jasnym sygnałem: „nie znaleziono zasobu”.

Pierwsze spotkanie ze Spring Boot: o co chodzi z tą „magią”
Spring i Spring Boot – jak to się łączy
Spring jako framework to ogromny ekosystem: DI, AOP, obsługa danych, bezpieczeństwo, integracje – można się w tym zgubić. Spring Boot powstał, żeby uprościć start nowej aplikacji. Zamiast ręcznego ustawiania dziesiątek zależności i konfiguracji, dostajesz:
- domyślne ustawienia dla typowych przypadków,
- zestawy zależności pogrupowane w tzw. Spring Boot Starters,
- wbudowany serwer aplikacji (np. Tomcat),
- mechanizm automatycznej konfiguracji (auto-configuration).
Dzięki temu pierwszy projekt Spring Boot można postawić w kilka minut, zamiast walczyć z plikami XML czy konfiguracją serwera.
Spring Boot Starters – koniec „piekła konfiguracji”
Startery to specyficzne zależności Maven/Gradle, które zawierają w sobie zestawy innych bibliotek dobranych pod konkretny cel. Na przykład:
spring-boot-starter-web– wszystko, czego potrzeba do REST API (Spring MVC, Jackson, wbudowany Tomcat),spring-boot-starter-data-jpa– Spring Data JPA i Hibernate do pracy z bazą danych,spring-boot-starter-test– biblioteki do testów jednostkowych i integracyjnych.
Zamiast samodzielnie dobierać wersje kilkunastu zależności, dodajesz jeden starter i Spring Boot dobiera restę za ciebie. To eliminuje wiele problemów z niekompatybilnymi wersjami bibliotek i przyspiesza start projektu.
Kluczowe adnotacje na start
Początkujący często widzą całą ścianę adnotacji i przestają rozumieć, co robi kod. Na start wystarczy kilka z nich:
- @SpringBootApplication – na klasie głównej; informuje Spring Boot, że tu jest punkt startowy aplikacji, włącza skanowanie komponentów i auto-konfigurację.
- @RestController – mówi, że dana klasa wystawia endpointy HTTP i każda metoda zwraca dane bezpośrednio w odpowiedzi (domyślnie JSON).
- @GetMapping, @PostMapping, @PutMapping, @DeleteMapping – mapowanie konkretnej metody HTTP i ścieżki URL na metodę w kontrolerze.
- @Service – oznaczenie klasy jako serwisu z logiką biznesową.
- @Repository – komponent odpowiedzialny za dostęp do danych (np. baza, pamięć).
Znając tylko te adnotacje, można już zbudować pełnoprawne REST API: kontroler, serwis i warstwę dostępu do danych.
Kontener IoC – co dzieje się przy starcie aplikacji
Kiedy uruchamiasz metodę main w klasie oznaczonej @SpringBootApplication, Spring Boot tworzy tzw. kontener IoC (Inversion of Control). W praktyce oznacza to, że:
- skanuje pakiety w poszukiwaniu klas z adnotacjami typu @RestController, @Service, @Repository,
- tworzy ich instancje (tzw. beany),
- łączy je ze sobą według deklarowanych zależności (np. przez konstruktor),
- uruchamia wbudowany serwer HTTP.
Od tej chwili nie ty tworzysz obiekty ręcznie – robisz to tylko pośrednio, deklarując je jako komponenty i zależności. To duża zmiana myślenia, ale w zamian zyskujesz mniej kodu „klejącego” i lepsze możliwości testowania.
Mini-wniosek: mniej kopiuj-wklej, więcej zrozumienia
Zrozumienie, że Spring Boot to przede wszystkim kontener zarządzający obiektami i ich konfiguracją, odcina dużą część frustracji. Każdy starter, każda adnotacja ma konkretne zadanie. Zamiast ślepo powielać przykład z tutoriala, lepiej zadać jedno pytanie: „co ta adnotacja mówi Springowi o tej klasie/metodzie?”. To podejście procentuje przy każdym kolejnym projekcie.
Zakładamy warsztat: narzędzia i konfiguracja środowiska krok po kroku
Wybór IDE: IntelliJ, Eclipse czy VS Code
Narzędzie pracy ma ogromny wpływ na tempo nauki. Do Spring Boot nada się kilka popularnych opcji:
- IntelliJ IDEA Community – darmowa, świetne wsparcie dla Javy, wygodny refactoring, dobre wsparcie Mavena/Gradle; do większości nauki Spring Boot wystarczy.
- IntelliJ IDEA Ultimate – płatna, lepsze wsparcie dla Springa (nawigacja po beanach, widoczność kontekstu), przyspiesza pracę w większych projektach.
- Eclipse – darmowy, mocno rozbudowany, ale czasem cięższy w konfiguracji; wielu programistów z niego wychodziło, ale część przesiada się później na IntelliJ.
- VS Code – lekki edytor z rozszerzeniami dla Javy i Spring Boot; dobry na szybki start, ale wymagający doinstalowania kilku pluginów.
Na pierwszy projekt Spring Boot zwykle najprościej zacząć od IntelliJ IDEA Community. Ma wygodną integrację ze Spring Initializr i dobrze współpracuje z Mavenem i Gradle.
Instalacja JDK i konfiguracja JAVA_HOME
Spring Boot 3+ wymaga Javy 17 lub nowszej (LTS). Dobrym wyborem jest OpenJDK 17 – wersja stabilna i długo wspierana. Po instalacji koniecznie sprawdź:
java -version
javac -versionW systemie warto ustawić zmienną środowiskową JAVA_HOME wskazującą na katalog z JDK. Wiele narzędzi (Maven, Gradle, IDE) korzysta właśnie z niej. Jeśli po wpisaniu w terminalu java -version pojawi się wersja 17 lub wyższa, jesteś na dobrej drodze.
Maven vs Gradle – co wybrać na początek
Spring Boot wspiera dwa główne narzędzia do budowania projektów: Maven i Gradle. Oba robią podobne rzeczy, ale różnią się filozofią i składnią.
| Narzędzie | Główny plik konfiguracyjny | Składnia | Na start |
|---|---|---|---|
| Maven | pom.xml | XML, bardziej deklaratywna | Łatwiejszy do czytania dla początkujących |
| Gradle | build.gradle | Groovy/Kotlin, bardziej „skryptowy” | Większa elastyczność, ale trudniejszy na start |
Dla pierwszego projektu Spring Boot najczęściej wygodniejszy będzie Maven – czytelny pom.xml pomaga zrozumieć, jakie dokładnie zależności znajdują się w projekcie. Gdy poczujesz się pewniej, bez problemu przerzucisz się na Gradle, jeśli będziesz tego potrzebować.
Lombok – wygoda, ale i potencjalne źródło frustracji
Lombok to biblioteka generująca automatycznie gettery, settery, konstruktory czy buildery na podstawie adnotacji. Brzmi świetnie, ale ma haczyk: wymaga dobrze skonfigurowanego IDE. Typowa sytuacja początkującego:
- kod się kompiluje,
- ale IDE pokazuje błędy w klasach z @Getter/@Setter,
- podpowiedzi nie działają, a wszystko wygląda „na czerwono”.
Żeby uniknąć takich problemów:
- zainstaluj plugin Lombok w IDE (np. w IntelliJ: Settings → Plugins → Lombok),
Konfiguracja projektu w IDE i pierwsze uruchomienie
Przychodzi ten moment: projekt utworzony przez Spring Initializr jest już w katalogu, IDE wszystko wczytało, ale po wciśnięciu „Run” pojawia się czerwony stos błędów. Zamiast pierwszego sukcesu – rozczarowanie i pytanie, co poszło nie tak. Kilka prostych kroków na starcie oszczędza takich niespodzianek.
Po zaimportowaniu projektu do IDE dobrze jest przejść krótką checklistę:
- czy projekt buduje się z poziomu Mavena/Gradle (
mvn clean installlub./mvnw spring-boot:run), - czy w drzewie katalogów widać standardową strukturę
src/main/javaisrc/test/java, - czy klasa z metodą
mainjest oznaczona @SpringBootApplication i znajduje się w „głównym” pakiecie projektu.
Jeśli wszystko wygląda sensownie, można spróbować uruchomić aplikację z IDE. W IntelliJ wystarczy otworzyć klasę główną (np. DemoApplication) i kliknąć zieloną strzałkę obok metody main. W konsoli powinna pojawić się seria logów, zakończona komunikatem w stylu:
Started DemoApplication in 2.345 seconds (JVM running for 2.789)To sygnał, że kontener Springa wstał, wbudowany serwer (zwykle Tomcat) nasłuchuje, a aplikacja jest dostępna pod adresem http://localhost:8080. Jeśli port jest zajęty (częsty przypadek, gdy działa inne API), logi pokażą błąd z informacją o konflikcie portu – wtedy można zmienić port w konfiguracji.
Dobry nawyk od pierwszego projektu: gdy coś nie działa, najpierw sprawdź logi Spring Boot w konsoli. Często jasno wskazują przyczynę: brakująca zależność, konflikt portu, błąd w konfiguracji bazy.
Plik application.properties / application.yml – pierwsze ustawienia
Prędzej czy później pojawia się potrzeba zmiany portu, konfiguracji logowania czy podpięcia bazy danych. Tym wszystkim zarządza plik konfiguracyjny Spring Boot – najczęściej application.properties lub application.yml w katalogu src/main/resources.
Na start wygodniejszy jest application.properties, bo ma prostą składnię klucz=wartość. Przykładowe minimalne ustawienia:
# zmiana portu HTTP
server.port=8081
# poziom logów
logging.level.root=INFO
logging.level.org.springframework.web=DEBUGPo zapisaniu pliku i ponownym uruchomieniu aplikacji Spring Boot automatycznie zastosuje nowe wartości. Nie trzeba kompilować projektu ręcznie ani niczego „odświeżać” – konfiguracja jest wczytywana przy starcie.
Mini-wniosek: zanim dodasz kolejną adnotację lub bibliotekę, często wystarczy pojedyncza linijka w application.properties, żeby osiągnąć efekt, o który chodzi.

Tworzenie projektu Spring Boot od zera: Initializr i struktura katalogów
Spring Initializr – szybki start bez ręcznego klejenia pom.xml
Wyobraź sobie, że musisz ręcznie pisać pom.xml, ustawiać wersję Javy, dodawać wszystkie zależności, tworzyć strukturę katalogów i klasę główną. Da się, ale cała energia idzie w konfigurację, a nie w pisanie logiki. Spring Initializr ten etap skraca do kilku kliknięć.
Na koniec warto zerknąć również na: Jak używać zadań domowych z deadline’ami, żeby motywować, a nie stresować — to dobre domknięcie tematu.
Klasyczny scenariusz wygląda tak:
- Wchodzisz na https://start.spring.io.
- Wybierasz:
- Project: Maven Project,
- Language: Java,
- Spring Boot: aktualną stabilną wersję (np. 3.x),
- Project Metadata: Group (np.
pl.twoja.firma), Artifact (np.task-api).
- Ustawiasz Packaging na
jar, a Java na 17 (lub wyżej, jeśli środowisko na to pozwala). - W sekcji Dependencies dodajesz:
Spring Web– do budowy REST API,- opcjonalnie
Spring Data JPAiH2 Database– jeśli planujesz CRUD z bazą.
- Klikasz „Generate” i pobierasz archiwum ZIP z gotowym szkieletem projektu.
Po rozpakowaniu paczki otrzymujesz minimalnie działający projekt, który wymaga tylko zaimportowania do IDE i uruchomienia. Reszta to już tworzenie własnych klas.
Struktura katalogów – co gdzie leży
Na pierwszy rzut oka w nowo utworzonym projekcie jest kilka katalogów i plików, które nic nie mówią. W praktyce ten układ jest spójny praktycznie we wszystkich projektach Spring Boot i dobrze go oswoić od razu.
projekt/
├─ src/
│ ├─ main/
│ │ ├─ java/
│ │ │ └─ pl/twoja/firma/taskapi/
│ │ │ └─ TaskApiApplication.java
│ │ └─ resources/
│ │ ├─ application.properties
│ │ └─ static/ (opcjonalnie)
│ └─ test/
│ └─ java/
│ └─ pl/twoja/firma/taskapi/
├─ pom.xml
└─ mvnw / mvnw.cmd (skrypty Mavena)Najważniejsze elementy na start:
TaskApiApplication.java– klasa z metodąmain, oznaczona @SpringBootApplication, punkt wejścia do aplikacji.- katalog
pl/twoja/firma/taskapi/– miejsce na twoje klasy: kontrolery, serwisy, encje. application.properties– wspomniany już plik konfiguracyjny.pom.xml– definicja zależności i ustawień Mavena.
Jedna praktyczna zasada: nie twórz klas poza pakietem głównym (tym, w którym leży klasa z @SpringBootApplication). Spring domyślnie skanuje pakiety potomne od tej klasy w dół. Jeśli stworzysz kontroler w innym drzewie pakietów, kontener może go po prostu nie znaleźć.
Rozsądny podział na pakiety w małej aplikacji
Nawet w małym projekcie lepiej od początku wprowadzić minimalny porządek. Jedna klasa „wszystko robiąca” kusi, ale bardzo szybko robi się z niej chaos. Spokojny, prosty podział wygląda często tak:
pl.twoja.firma.taskapi
├─ controller
├─ service
├─ repository
├─ model
└─ dto (opcjonalnie na później)W praktyce oznacza to:
controller– klasy z @RestController, obsługa żądań HTTP, zwracanie odpowiedzi,service– logika biznesowa, metody operujące na danych (np. „utwórz zadanie”, „zakończ zadanie”),repository– warstwa dostępu do danych (dla JPA: interfejsy rozszerzająceJpaRepository),model– klasy reprezentujące dane domenowe (np. encje JPA).
Mini-wniosek: im wcześniej rozdzielisz odpowiedzialności między pakietami, tym mniej refaktoryzacji czeka cię później, gdy aplikacja urośnie.
Pierwszy działający kontroler REST: od pustego projektu do odpowiedzi JSON
Najprostszy możliwy endpoint – „Hello REST”
W pewnym momencie pojawia się pytanie: „OK, aplikacja działa, ale gdzie jest ta część, która odpowiada na HTTP?”. Odpowiedzią jest kontroler REST. Na początek wystarczy jedna klasa z jedną metodą.
Utwórz pakiet controller i w nim klasę GreetingController:
package pl.twoja.firma.taskapi.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
@GetMapping("/hello")
public String hello() {
return "Witaj w swoim pierwszym API REST!";
}
}Po ponownym uruchomieniu aplikacji otwórz przeglądarkę lub narzędzie typu Postman/Insomnia i wywołaj:
GET http://localhost:8080/helloOdpowiedź powinna zawierać zwrócony tekst. To jeszcze nie jest JSON, ale pokazuje cały przepływ: żądanie trafia na ścieżkę /hello, Spring znajduje pasującą metodę w kontrolerze i zwraca rezultat do klienta.
Prosty endpoint zwracający JSON
Tekst jest dobry na start, ale w praktyce w API operujesz na strukturach danych – najczęściej w JSON. Spring Boot, dzięki Jacksonowi (dołączonemu przez spring-boot-starter-web), automatycznie serializuje obiekty Javy do JSON.
Dodaj prostą klasę modelu, np. Greeting w pakiecie model:
package pl.twoja.firma.taskapi.model;
public class Greeting {
private String message;
private String author;
public Greeting(String message, String author) {
this.message = message;
this.author = author;
}
public String getMessage() {
return message;
}
public String getAuthor() {
return author;
}
}Następnie zmodyfikuj kontroler tak, by zwracał obiekt:
package pl.twoja.firma.taskapi.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import pl.twoja.firma.taskapi.model.Greeting;
@RestController
public class GreetingController {
@GetMapping("/greeting")
public Greeting greeting() {
return new Greeting("Pierwszy JSON z Spring Boot", "System");
}
}Wywołanie:
GET http://localhost:8080/greetingzwróci coś w tym stylu:
{
"message": "Pierwszy JSON z Spring Boot",
"author": "System"
}Żadnej ręcznej konwersji, żadnej zabawy w parsowanie – Spring i Jackson robią to za kulisami.
Parametry w ścieżce i query string – dynamiczne odpowiedzi
Statyczna odpowiedź szybko przestaje wystarczać. Częściej potrzebujesz endpointów przyjmujących parametry, np. identyfikator zasobu lub filtr wyszukiwania. W Spring Boot realizuje się to dzięki adnotacjom @PathVariable i @RequestParam.
Dobrym uzupełnieniem będzie też materiał: Jak projektować aplikacje cloud-native w Javie? — warto go przejrzeć w kontekście powyższych wskazówek.
Prosty przykład z parametrem ścieżki:
@GetMapping("/greeting/{name}")
public Greeting greetingByName(@PathVariable String name) {
return new Greeting("Cześć, " + name, "System");
}Wywołanie:
GET http://localhost:8080/greeting/Aniazwróci JSON z komunikatem powitalnym dla konkretnego imienia.
Parametr zapytania (query) wygląda podobnie:
@GetMapping("/greeting-query")
public Greeting greetingWithQuery(@RequestParam(defaultValue = "Gość") String name) {
return new Greeting("Cześć, " + name, "System");
}Wywołania:
GET http://localhost:8080/greeting-query
GET http://localhost:8080/greeting-query?name=Kasiazwracają odpowiednio powitanie z domyślnym imieniem i imieniem przekazanym w URL. Ten sam mechanizm później wykorzystasz przy filtrach, paginacji czy sortowaniu.
Prosty POST – przyjmowanie danych w JSON
Scenka z życia: front-endowiec pisze UI, które ma wysyłać formularz z danymi nowego obiektu (np. zadania). Potrzebuje endpointu, który przyjmie JSON, przetworzy go i zwróci potwierdzenie. Tu wkracza @PostMapping i @RequestBody.
Załóżmy prosty model zadania w pamięci. Klasa Task:
package pl.twoja.firma.taskapi.model;
public class Task {
private Long id;
private String title;
private boolean done;
public Task() {
}
public Task(Long id, String title, boolean done) {
this.id = id;
this.title = title;
this.done = done;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public boolean isDone() {
return done;
}
public void setId(Long id) {
this.id = id;
}
public void setTitle(String title) {
this.title = title;
}
public void setDone(boolean done) {
this.done = done;
}
}W kontrolerze dodaj endpoint POST:
package pl.twoja.firma.taskapi.controller;
import org.springframework.web.bind.annotation.*;
import pl.twoja.firma.taskapi.model.Task;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
@RestController
@RequestMapping("/tasks")
public class TaskController {
private final List<Task> tasks = new ArrayList<>();
private final AtomicLong idGenerator = new AtomicLong(1);
@PostMapping
public Task createTask(@RequestBody Task task) {
task.setId(idGenerator.getAndIncrement());
tasks.add(task);
return task;
}
@GetMapping
public List<Task> getAllTasks() {
return tasks;
}
}Przykładowe wywołanie POST (np. z Postmana):
POST http://localhost:8080/tasks
Content-Type: application/json
{
"title": "Pierwsze zadanie",
"done": false
}Odpowiedź:
{
"id": 1,
"title": "Pierwsze zadanie",
"done": false
}Wywołanie:
GET http://localhost:8080/taskszwróci listę dotychczas utworzonych zadań. Na razie przechowywane są tylko w pamięci, czyli znikną po restarcie aplikacji, ale to wystarczy, aby zrozumieć przepływ danych i strukturę prostego API.

Projekt przykładowy: proste API CRUD krok po kroku
Wprowadzenie operacji CRUD – pełny cykl życia zadania
W pewnym momencie lista zadań w pamięci zaczyna żyć własnym życiem: jedno zadanie trzeba oznaczyć jako wykonane, inne poprawić, jeszcze inne usunąć. Pada pytanie: „to gdzie te wszystkie operacje mają mieszkać i jak je wystawić na REST?”. Tu wjeżdża klasyczny zestaw CRUD – Create, Read, Update, Delete.
Obecny kontroler ma już CREATE (POST) i prosty READ (GET wszystkich). Brakuje odczytu pojedynczego zadania, aktualizacji oraz usuwania.
Odczyt jednego zadania po ID (READ)
Zaczyna się niewinnie: front-end potrzebuje „szczegółów zadania” po kliknięciu w listę. Endpoint musi przyjąć identyfikator i zwrócić jedno dopasowane zadanie albo błąd, jeśli go nie ma.
Rozszerz istniejący TaskController o metodę GET po ID:
@GetMapping("/{id}")
public Task getTaskById(@PathVariable Long id) {
return tasks.stream()
.filter(task -> task.getId().equals(id))
.findFirst()
.orElseThrow(() -> new RuntimeException("Task not found"));
}Wywołanie:
GET http://localhost:8080/tasks/1zwróci zadanie o ID = 1, jeśli takie istnieje. Błąd jest jeszcze dość „surowy” (goły RuntimeException), ale to dobry punkt wyjścia do późniejszej obsługi wyjątków.
Mini-wniosek: już na wczesnym etapie warto odróżniać operacje na wielu zasobach (/tasks) od operacji na jednym, konkretnym zasobie (/tasks/{id}).
Aktualizacja zadania (UPDATE) – pełne zastąpienie obiektu
Typowa scena z aplikacji: użytkownik pomylił się w tytule zadania albo chce je oznaczyć jako ukończone. Potrzebny jest endpoint, który przyjmie nowe dane zadania i nadpisze stare.
Do aktualizacji często używa się metody HTTP PUT. Dodaj do TaskController:
@PutMapping("/{id}")
public Task updateTask(@PathVariable Long id, @RequestBody Task updatedTask) {
Task existingTask = tasks.stream()
.filter(task -> task.getId().equals(id))
.findFirst()
.orElseThrow(() -> new RuntimeException("Task not found"));
existingTask.setTitle(updatedTask.getTitle());
existingTask.setDone(updatedTask.isDone());
return existingTask;
}Wywołanie:
PUT http://localhost:8080/tasks/1
Content-Type: application/json
{
"title": "Poprawione zadanie",
"done": true
}nadpisze tytuł i status done istniejącego zadania o ID = 1.
Mini-wniosek: PUT to operacja „idempotentna” – wielokrotne wywołanie z tymi samymi danymi powinno dać ten sam efekt. Tak opłaca się projektować endpointy aktualizujące całe zasoby.
Częściowa aktualizacja (UPDATE) – PATCH dla zmiany pojedynczego pola
W praktyce bardzo często zmienia się tylko jedno pole – np. „oznacz zadanie jako ukończone”. Przesyłanie całego obiektu za każdym razem jest nieco nadmiarowe. Z pomocą przychodzi PATCH.
Przykładowy, prosty endpoint do zmiany statusu zadania:
@PatchMapping("/{id}/done")
public Task markTaskAsDone(@PathVariable Long id) {
Task existingTask = tasks.stream()
.filter(task -> task.getId().equals(id))
.findFirst()
.orElseThrow(() -> new RuntimeException("Task not found"));
existingTask.setDone(true);
return existingTask;
}Wywołanie:
PATCH http://localhost:8080/tasks/5/doneustawi done = true dla zadania o ID = 5. W wersji produkcyjnej często przyjmuje się ciało żądania z polami do zmiany, ale na start minimalistyczny wariant jest czytelniejszy.
Usuwanie zadania (DELETE) – koniec cyklu
Lista zadań bez usuwania szybko zamienia się w śmietnik. Endpoint HTTP DELETE porządkuje temat.
Dodaj do kontrolera:
@DeleteMapping("/{id}")
public void deleteTask(@PathVariable Long id) {
tasks.removeIf(task -> task.getId().equals(id));
}Wywołanie:
DELETE http://localhost:8080/tasks/1usunie zadanie o ID = 1 z listy w pamięci. Spring domyślnie zwróci kod statusu 200 lub 204 (w zależności od konfiguracji i typu zwracanej wartości).
Mini-wniosek: nazwy ścieżek pozostają spójne (/tasks/{id}), a to metoda HTTP (GET, PUT, PATCH, DELETE) mówi, co dzieje się z zasobem.
Rozdzielenie logiki – wprowadzenie serwisu TaskService
Po dodaniu kilku metod w kontrolerze zaczyna się pojawiać zgrzyt: obsługa HTTP miesza się z operacjami na liście, wyszukiwaniem i prostą „logiką biznesową”. Pierwszy krok do porządku to wydzielenie serwisu.
Warto też podejrzeć, jak ten temat rozwija Programista Java – kursy online, blog i praktyczne projekty — znajdziesz tam więcej inspiracji i praktycznych wskazówek.
Utwórz pakiet service i w nim klasę TaskService:
package pl.twoja.firma.taskapi.service;
import org.springframework.stereotype.Service;
import pl.twoja.firma.taskapi.model.Task;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class TaskService {
private final List<Task> tasks = new ArrayList<>();
private final AtomicLong idGenerator = new AtomicLong(1);
public Task createTask(Task task) {
task.setId(idGenerator.getAndIncrement());
tasks.add(task);
return task;
}
public List<Task> getAllTasks() {
return tasks;
}
public Task getTaskById(Long id) {
return tasks.stream()
.filter(task -> task.getId().equals(id))
.findFirst()
.orElseThrow(() -> new RuntimeException("Task not found"));
}
public Task updateTask(Long id, Task updatedTask) {
Task existingTask = getTaskById(id);
existingTask.setTitle(updatedTask.getTitle());
existingTask.setDone(updatedTask.isDone());
return existingTask;
}
public Task markTaskAsDone(Long id) {
Task existingTask = getTaskById(id);
existingTask.setDone(true);
return existingTask;
}
public void deleteTask(Long id) {
tasks.removeIf(task -> task.getId().equals(id));
}
}Następnie „odchudź” kontroler, wstrzykując serwis przez konstruktor:
package pl.twoja.firma.taskapi.controller;
import org.springframework.web.bind.annotation.*;
import pl.twoja.firma.taskapi.model.Task;
import pl.twoja.firma.taskapi.service.TaskService;
import java.util.List;
@RestController
@RequestMapping("/tasks")
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@PostMapping
public Task createTask(@RequestBody Task task) {
return taskService.createTask(task);
}
@GetMapping
public List<Task> getAllTasks() {
return taskService.getAllTasks();
}
@GetMapping("/{id}")
public Task getTaskById(@PathVariable Long id) {
return taskService.getTaskById(id);
}
@PutMapping("/{id}")
public Task updateTask(@PathVariable Long id, @RequestBody Task updatedTask) {
return taskService.updateTask(id, updatedTask);
}
@PatchMapping("/{id}/done")
public Task markTaskAsDone(@PathVariable Long id) {
return taskService.markTaskAsDone(id);
}
@DeleteMapping("/{id}")
public void deleteTask(@PathVariable Long id) {
taskService.deleteTask(id);
}
}Mini-wniosek: kontroler odpowiada za HTTP (ścieżki, statusy, typy odpowiedzi), a serwis za „co” ma się wydarzyć z danymi. Takie rozdzielenie procentuje przy każdej większej zmianie.
Przejście z pamięci na bazę danych – pierwsze kroki z Spring Data JPA
Prosta lista w pamięci działa zaskakująco długo… do momentu, kiedy trzeba zachować dane po restarcie albo udostępnić API innym ludziom w zespole. Naturalnym kolejnym krokiem jest podpięcie bazy danych, najczęściej relacyjnej (np. PostgreSQL, MySQL, lokalnie H2).
Do integracji z bazą w Springu idealnie nadaje się Spring Data JPA. W projekcie z Mavenem upewnij się, że masz zależność:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>Do nauki wygodny jest wbudowany H2 – lekka baza w pamięci:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>Przekształcenie Task w encję JPA
Model Task trzeba teraz „nauczyć” współpracy z bazą – robi się to za pomocą adnotacji JPA.
Zmień klasę Task w pakiecie model:
package pl.twoja.firma.taskapi.model;
import jakarta.persistence.*;
@Entity
@Table(name = "tasks")
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private boolean done;
public Task() {
}
public Task(String title, boolean done) {
this.title = title;
this.done = done;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public boolean isDone() {
return done;
}
public void setId(Long id) {
this.id = id;
}
public void setTitle(String title) {
this.title = title;
}
public void setDone(boolean done) {
this.done = done;
}
}Najważniejsze elementy:
@Entity– informacja, że klasa jest encją JPA i ma być mapowana na tabelę,@Table(name = "tasks")– nazwa tabeli w bazie (jeśli jej nie podasz, JPA samo ją wygeneruje),@Id+@GeneratedValue– klucz główny i strategia generowania ID.
Mini-wniosek: encja to klasa, którą JPA potrafi zapisać i odczytać z tabeli; dzięki adnotacjom nie piszesz ręcznie SQL-a przy każdej operacji CRUD.
Repository – interfejs zamiast ręcznego DAO
Najprzyjemniejszy moment w Spring Data JPA to ten, w którym odkrywasz, że większości metod nie musisz pisać samodzielnie. Wystarczy interfejs rozszerzający JpaRepository.
Utwórz pakiet repository i interfejs TaskRepository:
package pl.twoja.firma.taskapi.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import pl.twoja.firma.taskapi.model.Task;
public interface TaskRepository extends JpaRepository<Task, Long> {
}Ten jeden interfejs daje od razu gotowe metody m.in.:
save(Task entity)findById(Long id)findAll()deleteById(Long id)
Bez ani jednego zapytania SQL. Później można dodawać metody typu findByDone(boolean done), a Spring wygeneruje im implementację na podstawie nazwy.
Przepisanie TaskService na repozytorium JPA
Serwis nadal używa listy w pamięci. Czas, by operacje CRUD „spadły” na bazę danych za pośrednictwem repozytorium.
Zmień TaskService tak, aby wstrzykiwał TaskRepository:
package pl.twoja.firma.taskapi.service;
import org.springframework.stereotype.Service;
import pl.twoja.firma.taskapi.model.Task;
import pl.twoja.firma.taskapi.repository.TaskRepository;
import java.util.List;
@Service
public class TaskService {
private final TaskRepository taskRepository;
public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
public Task createTask(Task task) {
return taskRepository.save(task);
}
public List<Task> getAllTasks() {
return taskRepository.findAll();
}
public Task getTaskById(Long id) {
return taskRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Task not found"));
}
public Task updateTask(Long id, Task updatedTask) {
Task existingTask = getTaskById(id);
existingTask.setTitle(updatedTask.getTitle());
existingTask.setDone(updatedTask.isDone());
return taskRepository.save(existingTask);
}
public Task markTaskAsDone(Long id) {
Task existingTask = getTaskById(id);
existingTask.setDone(true);
return taskRepository.save(existingTask);
}
public void deleteTask(Long id) {
taskRepository.deleteById(id);
}
}Kontroler nie wymaga żadnych zmian – jego kontrakt REST pozostaje ten sam. Zmieniło się wyłącznie „zaplecze”, czyli implementacja serwisu.
Mini-wniosek: kiedy kontroler i serwis są dobrze odseparowane, wymiana warstwy danych (pamięć → baza) może odbyć się bez dotykania endpointów widocznych dla świata.
Konfiguracja bazy danych H2 w application.properties
Żeby JPA wiedziało, z jaką bazą ma pracować, trzeba dodać prostą konfigurację. Dla H2 (baza w pamięci) wystarczy kilka wpisów w src/main/resources/application.properties:
spring.datasource.url=jdbc:h2:mem:taskdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.h2.console.enabled=true
spring.h2.console.path=/h2-consoleCo tutaj się dzieje:
jdbc:h2:mem:taskdb– baza w pamięci o nazwietaskdb,ddl-auto=update– Hibernate sam tworzy/aktualizuje strukturę tabel na podstawie encji,show-sql=true– zapytania SQL będą widoczne w logach (świetne do nauki),
Najważniejsze punkty
- Pewne opanowanie Javy (klasy, interfejsy, kolekcje, wyjątki, podstawy Java 8) jest warunkiem wejścia w Spring Boot – bez tego nawet prosta metoda serwisu czy stacktrace z błędem stają się nieczytelną zagadką.
- Spring Boot nie zastępuje myślenia o architekturze: Dependency Injection tylko automatyzuje tworzenie obiektów, więc trzeba świadomie projektować zależności między klasami i wiedzieć, gdzie trzymać logikę, a gdzie samo delegowanie.
- Przeskok z aplikacji konsolowej na webową polega na zmianie sposobu myślenia: zamiast jednego ciągłego „flow” pojawia się cykl request–response, rozdział na kontrolery, serwisy i warstwę danych oraz działający w tle serwer HTTP.
- Znajomość podstaw HTTP i REST (metody, statusy, ścieżki URL, body w JSON) sprawia, że adnotacje typu @GetMapping czy @PostMapping przestają być „magicznymi słowami” i od razu wiadomo, jaki kontrakt API się właśnie definiuje.
- Spring to ogromny ekosystem, a Spring Boot jest jego „skrótem startowym” – dostarcza automatyczną konfigurację, wbudowany serwer i gotowe zestawy zależności, dzięki czemu można skupić się na kodzie biznesowym zamiast na plikach XML i ręcznym stawianiu środowiska.
- Świadome budowanie pierwszej aplikacji REST w Spring Boot polega na połączeniu kilku fundamentów: Javy, HTTP, podziału na warstwy (kontroler–serwis–repozytorium) i zrozumienia, co dokładnie Spring robi „pod spodem”.
Bibliografia
- Spring Boot Reference Documentation. VMware – Oficjalna dokumentacja Spring Boot: auto-konfiguracja, startery, adnotacje
- Spring Framework Reference Documentation. VMware – Podstawy Spring: DI, kontener IoC, komponenty, architektura aplikacji
- RESTful Web Services. O’Reilly Media (2007) – Wprowadzenie do stylu REST, zasoby, metody HTTP, statusy
- Java Platform, Standard Edition 8 Language Specification. Oracle (2015) – Podstawy Javy 8: klasy, interfejsy, wyjątki, lambdy, strumienie
- Jakarta Servlet Specification. Eclipse Foundation – Serwlety, model żądań/odpowiedzi HTTP, rola kontenera (Tomcat)






