Javalin - Ćwiczenia demonstracyjne
Wstęp
Poniżej znajduje się zestaw ćwiczeń demonstracyjnych (hands-on), które obrazują praktyczne zastosowanie zagadnień z kursu Javalin. Każde ćwiczenie zawiera:
- Cel - co chcemy osiągnąć
- Wskazówki - jak to zrobić
- Kod - kompletne rozwiązanie
- Eksperymenty - jak eksperymentować dalej
Ćwiczenie 1: Twój pierwszy serwer Javalin
Cel
Uruchomić prosty serwer HTTP, który odpowiada „Hello” na żądanie GET do /
Wskazówki
1. Utwórz nowy projekt Maven w IntelliJ 2. Dodaj ''javalin-bundle'' do ''pom.xml'' 3. Utwórz klasę ''HelloApp'' z metodą ''main'' 4. Następnie uruchom projekt i otwórz przeglądarkę na ''http://localhost:8080''
Rozwiązanie
import io.javalin.Javalin; public class HelloApp { public static void main(String[] args) { Javalin app = Javalin.create(config -> { config.routes.get("/", ctx -> { ctx.result("Hello!"); }); }); app.start(8080); System.out.println("Serwer uruchomiony na http://localhost:8080"); } }
Eksperymenty
- Zmień port z 8080 na 3000, 9000, czy inny
- Dodaj więcej tras:
/hello,/world - Zmień odpowiedź w zależności od trasy
Ćwiczenie 2: Parametry w ścieżce (Path Parameters)
Cel
Stworzyć serwer, który powituje użytkownika po imieniu. Przykład: /greet/Alice → „Hello, Alice!”
Wskazówki
1. Użyj parametru w definicji trasy: ''/greet/{name}''
2. Pobierz parametr za pomocą ''ctx.pathParam("name")''
3. Zwróć spersonalizowaną wiadomość
Rozwiązanie
import io.javalin.Javalin; public class GreetApp { public static void main(String[] args) { Javalin app = Javalin.create(config -> { config.routes.get("/greet/{name}", ctx -> { String name = ctx.pathParam("name"); ctx.result("Hello, " + name + "!"); }); }); app.start(8080); System.out.println("Spróbuj: http://localhost:8080/greet/Alice"); } }
Eksperymenty
- Dodaj dwa parametry:
/greet/{firstName}/{lastName} - Dodaj konwersję na liczby:
/multiply/{a}/{b}i zwróć iloczyn - Obsłuż przypadek gdy parametr jest pusty (np.
/greet/):config.routes.get(„/greet/{name?}”, …)
Ćwiczenie 3: Query Parameters
Cel
Stworzyć wyszukiwanie. Użytkownik wpisuje zapytanie: /search?q=java&limit=5 a serwer zwraca wyniki.
Wskazówki
1. Użyj ''ctx.queryParam("q")'' do poboru parametru
2. Użyj ''ctx.queryParam("limit", "10")'' dla wartości domyślnej
3. Zwróć sformatowaną odpowiedź
Rozwiązanie
import io.javalin.Javalin; public class SearchApp { public static void main(String[] args) { Javalin app = Javalin.create(config -> { config.routes.get("/search", ctx -> { String query = ctx.queryParam("q", ""); String limit = ctx.queryParam("limit", "10"); ctx.result("Szukam: '" + query + "' (limit: " + limit + ")"); }); }); app.start(8080); System.out.println("Spróbuj: http://localhost:8080/search?q=java&limit=5"); } }
Eksperymenty
- Dodaj walidację - jeśli
qjest puste, zwróć błąd 400 - Obsługuj wiele tagów:
/search?tag=java&tag=web - Symuluj wyszukiwanie w bazie danych (na razie statyczne wyniki)
Ćwiczenie 4: POST i ciało żądania (Body)
Cel
Utwórz endpoint POST, który przyjmuje JSON z danymi użytkownika i zwraca je potwierdzenie.
Wskazówki
1. ZdefiniujRecord z danymi użytkownika 2. Użyj ''config.routes.post()'' zamiast ''config.routes.get()'' 3. Pobierz JSON za pomocą ''ctx.bodyAsClass(User.class)'' 4. Ustaw status 201 (Created) i zwróć JSON odpowiedzi
Rozwiązanie
import io.javalin.Javalin; record User(String name, String email) {} public class UserApp { public static void main(String[] args) { Javalin app = Javalin.create(config -> { config.routes.post("/users", ctx -> { User user = ctx.bodyAsClass(User.class); // Symulacja zapisania w bazie ctx.status(201); ctx.result("Użytkownik " + user.name() + " został utworzony!"); }); }); app.start(8080); System.out.println("Spróbuj: POST http://localhost:8080/users"); System.out.println("Body: {\"name\": \"Alice\", \"email\": \"alice@example.com\"}"); } }
Eksperymenty
- W Postman, utwórz POST request z JSON body
- Zwróć utworzony użytkownik jako JSON odpowiedź:
ctx.json(user) - Dodaj walidację - jeśli nazwa jest pusta, zwróć błąd 400
Ćwiczenie 5: Kody statusów HTTP i obsługa błędów
Cel
Stworzyć endpoint GET, który zwraca 404 jeśli użytkownik nie istnieje, 200 jeśli istnieje.
Wskazówki
1. Symuluj bazę użytkowników (Map) 2. Sprawdź czy użytkownik istnieje 3. Zwróć odpowiedni status i wiadomość
Rozwiązanie
import io.javalin.Javalin; import java.util.Map; public class UserGetApp { static Map<Integer, String> users = Map.of( 1, "Alice", 2, "Bob" ); public static void main(String[] args) { Javalin app = Javalin.create(config -> { config.routes.get("/users/{id}", ctx -> { int id = Integer.parseInt(ctx.pathParam("id")); if (users.containsKey(id)) { ctx.status(200); ctx.result("Użytkownik: " + users.get(id)); } else { ctx.status(404); ctx.result("Użytkownik nie znaleziony"); } }); }); app.start(8080); } }
Eksperymenty
- Zwróć JSON zamiast tekstu
- Obsługuj wyjątek NumberFormatException i zwróć 400
- Dodaj zwracanie 204 No Content dla DELETE
Ćwiczenie 6: JSON - Serializacja i deserializacja
Cel
Stworzyć API, które przesyła i odbiera obiekty w formacie JSON.
Wskazówki
1. Zdefiniuj Recordy z danymi 2. Użyj ''ctx.json(object)'' do wysłania 3. Użyj ''ctx.bodyAsClass(Class.class)'' do odboru
Rozwiązanie
import io.javalin.Javalin; import java.util.*; record Product(int id, String name, double price) {} public class ProductApp { static List<Product> products = new ArrayList<>(Arrays.asList( new Product(1, "Laptop", 999.99), new Product(2, "Mouse", 29.99) )); public static void main(String[] args) { Javalin app = Javalin.create(config -> { // GET - zwróć listę produktów config.routes.get("/products", ctx -> { ctx.json(products); }); // GET - zwróć jeden produkt config.routes.get("/products/{id}", ctx -> { int id = ctx.pathParamAsClass("id", Integer.class).get(); Product product = products.stream() .filter(p -> p.id() == id) .findFirst() .orElse(null); if (product != null) { ctx.json(product); } else { ctx.status(404).result("Produkt nie znaleziony"); } }); // POST - utwórz nowy produkt config.routes.post("/products", ctx -> { Product newProduct = ctx.bodyAsClass(Product.class); products.add(newProduct); ctx.status(201).json(newProduct); }); }); app.start(8080); } }
Eksperymenty
- Dodaj PUT dla aktualizacji
- Dodaj DELETE dla usunięcia
- Zwróć listę pól w odpowiedzi na POST
Ćwiczenie 7: Middleware - Logging
Cel
Dodać middleware, które loguje każde żądanie i odpowiedź.
Wskazówki
1. Użyj ''config.routes.before()'' do logowania żądania 2. Użyj ''config.routes.after()'' do logowania odpowiedzi 3. Wyświetl ścieżkę, metodę, status, czas przetwarzania
Rozwiązanie
import io.javalin.Javalin; public class LoggingApp { public static void main(String[] args) { Javalin app = Javalin.create(config -> { // Before - loguj żądanie config.routes.before(ctx -> { long startTime = System.currentTimeMillis(); ctx.attribute("startTime", startTime); System.out.println(">>> " + ctx.method() + " " + ctx.path()); }); // After - loguj odpowiedź config.routes.after(ctx -> { long startTime = ctx.attribute("startTime"); long duration = System.currentTimeMillis() - startTime; System.out.println("<<< Status: " + ctx.status() + " (" + duration + "ms)"); }); config.routes.get("/", ctx -> { ctx.result("Hello!"); }); }); app.start(8080); } }
Eksperymenty
- Zmień format logowania (JSON, CSV)
- Loguj tylko określone ścieżki:
config.routes.before(„/api/*”, …)
- Dodaj logowanie nagłówków lub body
Ćwiczenie 8: Walidacja - Before Handler
Cel
Dodać walidację, która sprawdza czy żądanie ma wymagany token autoryzacji.
Wskazówki
1. W ''before()'' pobierz nagłówek ''Authorization'' 2. Sprawdź czy token istnieje i jest ważny 3. Jeśli nie, zwróć status 401
Rozwiązanie
import io.javalin.Javalin; public class AuthApp { static String VALID_TOKEN = "secret123"; public static void main(String[] args) { Javalin app = Javalin.create(config -> { // Walidacja tokenu dla wszystkich endpointów /api config.routes.before("/api/*", ctx -> { String token = ctx.header("Authorization"); if (token == null || !token.equals("Bearer " + VALID_TOKEN)) { ctx.status(401); ctx.result("Unauthorized - brak ważnego tokenu"); } }); config.routes.get("/api/secret", ctx -> { ctx.result("To jest sekretna wiadomość!"); }); config.routes.get("/public", ctx -> { ctx.result("To jest publiczne"); }); }); app.start(8080); } }
Eksperymenty
- Zmień format tokenu (JWT, Base64)
- Loguj nieudane próby autoryzacji
- Zwróć JSON zamiast tekstu w przypadku błędu
Ćwiczenie 9: Obsługa błędów - Exception Handler
Cel
Obsłużyć wyjątki i zwrócić przyjazne komunikaty błędów.
Wskazówki
1. Utwórz własny wyjątek 2. Użyj obsługi wyjątków w konfiguracji aplikacji, np. ''config.exception()'' 3. Zwróć JSON z opisem błędu i statusem 400 lub 500
Rozwiązanie
import io.javalin.Javalin; import java.util.Map; class ValidationException extends Exception { ValidationException(String message) { super(message); } } record User(String name, String email) {} public class ErrorApp { public static void main(String[] args) { Javalin app = Javalin.create(config -> { config.exception(ValidationException.class, (e, ctx) -> { ctx.status(400); ctx.json(Map.of("error", e.getMessage())); }); config.exception(NumberFormatException.class, (e, ctx) -> { ctx.status(400); ctx.json(Map.of("error", "Liczba musi być liczbą całkowitą")); }); config.routes.post("/users", ctx -> { User user = ctx.bodyAsClass(User.class); if (user.name() == null || user.name().isEmpty()) { throw new ValidationException("Nazwa nie może być pusta"); } ctx.status(201).json(user); }); config.routes.get("/divide/{a}/{b}", ctx -> { int a = ctx.pathParamAsClass("a", Integer.class).get(); int b = ctx.pathParamAsClass("b", Integer.class).get(); int result = a / b; // Może rzucić ArithmeticException ctx.json(Map.of("result", result)); }); }); app.start(8080); } }
Eksperymenty
- Dodaj obsługę dla ArithmeticException (dzielenie przez zero)
- Dodaj ogólny exception handler dla wszystkich błędów
- Loguj błędy na konsoli
Ćwiczenie 10: Obsługa plików statycznych
Cel
Serwować plik HTML z folderu public.
Wskazówki
1. Utwórz folder ''src/main/resources/public''
2. Utwórz plik ''index.html''
3. W kodzie skonfiguruj: ''config.staticFiles.add("/public")''
4. Otwórz ''http://localhost:8080/index.html''
Struktura projektu
src/main/resources/
└── public/
├── index.html
├── css/
│ └── style.css
└── js/
└── app.js
index.html
<!DOCTYPE html> <html> <head> <title>Moja aplikacja</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <h1>Witaj w Javalin!</h1> <button id="btn">Kliknij mnie</button> <script src="/js/app.js"></script> </body> </html>
css/style.css
body { font-family: Arial, sans-serif; padding: 20px; } button { padding: 10px 20px; background-color: blue; color: white; cursor: pointer; }
js/app.js
document.getElementById('btn').addEventListener('click', function() { alert('Przycisk został kliknięty!'); });
Kod Javy
import io.javalin.Javalin; public class StaticApp { public static void main(String[] args) { Javalin app = Javalin.create(config -> { config.staticFiles.add("/public"); }); app.start(8080); System.out.println("Otwórz http://localhost:8080/index.html"); } }
Eksperymenty
- Dodaj więcej plików CSS i JavaScript
- Serwuj obrazy z folderu
images - Połącz API z frontend: w
app.jspobierz dane z backendu
Ćwiczenie 11: Pełny RESTful CRUD
Cel
Stworzyć kompletne CRUD API dla TODO. Wszystkie operacje: Create, Read, Update, Delete.
Wskazówki
1. Utwórz listę TODO w pamięci 2. GET - zwróć wszystkie lub jedno TODO 3. POST - utwórz nowe TODO 4. PUT - aktualizuj TODO 5. DELETE - usuń TODO
Rozwiązanie
import io.javalin.Javalin; import java.util.*; record Todo(int id, String title, String description, boolean completed) {} public class TodoCrudApp { static Map<Integer, Todo> todos = new HashMap<>(); static int nextId = 1; static { todos.put(1, new Todo(1, "Nauczyć się Javalin", "Obejrzeć kurs", false)); todos.put(2, new Todo(2, "Zrobić projekt", "Aplikacja Todo", true)); nextId = 3; } public static void main(String[] args) { Javalin app = Javalin.create(config -> { // GET - wszystkie TODO config.routes.get("/api/todos", ctx -> { ctx.json(new ArrayList<>(todos.values())); }); // GET - jedno TODO config.routes.get("/api/todos/{id}", ctx -> { int id = ctx.pathParamAsClass("id", Integer.class).get(); Todo todo = todos.get(id); if (todo != null) { ctx.json(todo); } else { ctx.status(404).json(Map.of("error", "TODO nie znaleziono")); } }); // POST - utwórz TODO config.routes.post("/api/todos", ctx -> { Map<String, Object> body = ctx.bodyAsClass(Map.class); String title = (String) body.get("title"); String description = (String) body.get("description"); Todo newTodo = new Todo(nextId, title, description, false); todos.put(nextId, newTodo); nextId++; ctx.status(201).json(newTodo); }); // PUT - aktualizuj TODO config.routes.put("/api/todos/{id}", ctx -> { int id = ctx.pathParamAsClass("id", Integer.class).get(); Map<String, Object> body = ctx.bodyAsClass(Map.class); if (!todos.containsKey(id)) { ctx.status(404).json(Map.of("error", "TODO nie znaleziono")); return; } Todo existing = todos.get(id); String newTitle = (String) body.getOrDefault("title", existing.title()); String newDesc = (String) body.getOrDefault("description", existing.description()); boolean newCompleted = (boolean) body.getOrDefault("completed", existing.completed()); Todo updated = new Todo(id, newTitle, newDesc, newCompleted); todos.put(id, updated); ctx.json(updated); }); // DELETE - usuń TODO config.routes.delete("/api/todos/{id}", ctx -> { int id = ctx.pathParamAsClass("id", Integer.class).get(); if (todos.containsKey(id)) { todos.remove(id); ctx.status(204); } else { ctx.status(404).json(Map.of("error", "TODO nie znaleziono")); } }); }); app.start(8080); System.out.println("TODO API dostępne na http://localhost:8080/api/todos"); } }
Eksperymenty
- Obsłużyć błędy bardziej elegancko
- Dodać walidację (tytuł nie może być pusty)
- Przetestować wszystkie operacje w Postmanie
Podsumowanie
Te ćwiczenia obejmują:
✓ Uruchamianie serwera ✓ Routing i parametry ✓ Obsługę żądań i odpowiedzi ✓ JSON ✓ Statusy HTTP ✓ Middleware ✓ Obsługę błędów ✓ Pliki statyczne ✓ Kompletny CRUD API
Po wykonaniu wszystkich ćwiczeń będziesz gotowy do samodzielnego tworzenia aplikacji w Javalin!