Request og response
Når en bruger arbejder med vores webapplikation, foregår der hele tiden en samtale mellem browseren og serveren.
Browseren sender en request. Serveren behandler requesten og sender et response tilbage. Det kaldes en request/response cycle.

I vores applikation kan cyklussen for eksempel se sådan ud:
Browser
↓
HTTP request
↓
Javalin / Controller
↓
Service
↓
Mapper
↓
Database
↓
Service
↓
Controller
↓
Thymeleaf
↓
HTTP response
↓
Browser
Ikke alle requests går hele vejen til databasen. Hvis brugeren for eksempel bare skal have vist en statisk side, kan flowet være meget kortere.
Det vigtige er at forstå, hvor de forskellige ting sker.
En HTTP-request sendes fra browseren til serveren.
En request indeholder blandt andet:
- en HTTP-metode
- en URL/path
- headers
- eventuelt data
Data, som browseren sender med requesten, kan blandt andet ligge i request body.
GET bruges typisk, når browseren beder serveren om noget.
For eksempel:
GET /users
Browseren siger i praksis:
“Jeg vil gerne have
/users.”
Query-parametre kan være en del af URL’en:
/users?id=42
I Javalin kan vi hente query-parametre:
String id = ctx.queryParam("id");
Flere query-parametre:
/users?id=42&sort=name
kan hentes sådan:
String id = ctx.queryParam("id");
String sort = ctx.queryParam("sort");
GET bruges typisk til at hente eller vise data.
POST bruges typisk, når browseren sender data til serveren, for eksempel fra en formular.
En loginformular kan sende:
POST /login
med data som:
username=ada
password=secret
I Javalin kan form-data hentes sådan:
String username = ctx.formParam("username");
String password = ctx.formParam("password");
POST bruges ofte til handlinger som:
- oprette en bruger
- logge ind
- oprette en task
- sende en formular
PUT og PATCH bruges typisk, når en eksisterende ressource skal opdateres.
De møder man især i API’er.
Eksempel:
PUT /users/42
betyder, at brugeren med ID 42 skal opdateres.
PATCH bruges typisk til en delvis opdatering.
I vores server-renderede applikation vil vi primært arbejde med GET og POST.
DELETE bruges til at slette en ressource.
Eksempel:
DELETE /users/42
Igen er dette især relevant i API-baserede applikationer.
I vores server-renderede applikation kan en slettefunktion i stedet eksempelvis være lavet som en formular, der sender en POST-request.
Requesten bliver modtaget af webserveren.
I vores Javalin-applikation er Javalin baseret på Jetty, som håndterer den underliggende HTTP-kommunikation.
Javalin giver os adgang til den aktuelle request gennem ctx.
config.routes.get("/users", ctx -> {
// ctx repræsenterer den aktuelle request/response
});
ctx er vores forbindelse til den aktuelle HTTP-request og response.
Vi kan blandt andet bruge ctx til at:
- hente data fra requesten
- hente headers
- hente form-parametre
- hente query-parametre
- hente path-parametre
- gemme data i sessionen
- sende data til en template
- sætte HTTP-status
- sende en redirect
- rendre en HTML-side
HTTP-requesten indeholder headers med information om requesten.
En header kan læses med:
String host = ctx.header("Host");
Der findes mange forskellige HTTP-headers.
De fleste af dem behøver vi ikke arbejde direkte med i vores almindelige applikationer.
Når Javalin har fundet den route, som passer til requesten, bliver route-handleren udført.
Et meget simpelt eksempel:
config.routes.get("/hello", ctx -> {
ctx.result("Hej verden");
});
Her sker der ikke andet end:
GET /hello
↓
route handler
↓
"Hej verden"
I en rigtig applikation skal der ofte ske mere.
For eksempel ved login:
POST /login
↓
hent username og password
↓
UserService.login(...)
↓
login lykkes?
Her kommer vores lagdeling ind.
Controlleren tager sig af kommunikationen mellem HTTP og resten af applikationen.
Controlleren kan for eksempel:
- hente data fra requesten
- kalde en service
- vælge hvad der skal ske bagefter
- sende et resultat tilbage til browseren
Eksempel:
public static void login(Context ctx, UserService userService) {
String username = ctx.formParam("username");
String password = ctx.formParam("password");
User user = userService.login(username, password);
if (user != null) {
ctx.sessionAttribute("user", user);
ctx.redirect("/tasks");
} else {
ctx.status(401);
ctx.attribute("error", "Forkert brugernavn eller password");
ctx.render("login.html");
}
}
Controlleren ved altså noget om HTTP. Den ved for eksempel, hvad formParam, redirect, status og render betyder.
Forretningslogikken skal ikke ligge i controlleren. Den placerer vi i en serviceklasse.
Eksempel:
public User login(String username, String password) {
User user = userMapper.getUserByUsername(username);
if (user != null &&
user.getPassword().equals(password)) {
return user;
}
return null;
}
Servicen arbejder med programmets regler.
Den skal ikke vide noget om:
- HTML
- Thymeleaf
- browseren
- HTTP-statuskoder
ctx
Det betyder, at den samme service kan bruges fra andre dele af programmet.
Hvis servicen skal hente eller gemme data i databasen, kalder den en mapper.
Controller
↓
Service
↓
Mapper
↓
Database
Mapperen tager sig af SQL og JDBC.
Eksempel:
public User getUserByUsername(String username) {
String sql = """
SELECT username, password
FROM users
WHERE username = ?
""";
// JDBC-kode
}
Controlleren behøver ikke vide, hvordan brugeren blev hentet.
Den ved bare, at den kan spørge servicen:
User user = userService.login(username, password);
Når requesten er behandlet, skal serveren sende et response tilbage til browseren.
Et response kan indeholde:
- en HTTP-statuskode
- headers
- en response body
I vores server-renderede applikation er response body ofte HTML.
Response body indeholder det indhold, som serveren sender tilbage til klienten. I Javalin kan vi for eksempel sætte response body med ctx.result().
Den enkleste response er:
ctx.result("Hej verden");
Her er “Hej verden” en streng, som sendes tilbage som response body. Når vi bruger ctx.result() til at sende tekst tilbage, sætter Javalin Content-Type til text/html i headeren, så browseren fortolker response body som HTML.
Browseren modtager teksten:
Hej verden
Vi kan sætte HTTP-statuskoden:
ctx.status(404);
eller kombinere status og tekst:
ctx.status(404).result("Brugeren blev ikke fundet");
Eksempler på statuskoder, som er gode at kende:
| Status | Betydning |
|---|---|
| 200 | OK |
| 201 | Created |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 500 | Internal Server Error |
Vi behøver ikke kunne alle HTTP-statuskoder udenad.
Det vigtigste er at forstå, at statuskoden fortæller klienten, hvordan requesten gik.
Serveren kan bede browseren om at gå til en anden URL:
ctx.redirect("/tasks");
Et typisk flow efter login er:
POST /login
↓
login lykkes
↓
ctx.redirect("/tasks")
↓
GET /tasks
Browseren foretager derefter en ny request til /tasks.
Når response skal være en HTML-side, bruger vi Thymeleaf.
Før vi renderer siden, kan controlleren lægge data i request scope:
ctx.attribute("user", user);
ctx.attribute("tasks", tasks);
Derefter renderer vi templaten:
ctx.render("tasks.html");
Thymeleaf bruger dataene til at generere den HTML, som sendes til browseren.
I templaten kan vi for eksempel skrive:
<h1 th:text="${user.username}">
Brugernavn
</h1>
Og:
<ul>
<li th:each="task : ${tasks}"
th:text="${task.title}">
Task
</li>
</ul>
Browseren modtager almindelig HTML.
Den kender ikke noget til Thymeleaf.
Nogle data skal kunne bruges på tværs af flere requests.
Et eksempel er den bruger, der er logget ind.
Vi kan gemme brugeren i sessionen:
ctx.sessionAttribute("user", user);
På en senere request kan vi hente brugeren:
User user = ctx.sessionAttribute("user");
Thymeleaf kan også læse sessionens attributter:
<p th:text="${session.user.username}">
Brugernavn
</p>
Sessionen gør det derfor muligt at huske oplysninger mellem requests.
Lad os følge et login fra start til slut.
Browseren viser loginformularen:
GET /login
Javalin finder login-routen:
config.routes.get("/login", ctx -> {
ctx.render("login.html");
});
Browseren viser formularen.
Brugeren udfylder:
username = ada
password = secret
og sender formularen:
POST /login
Controlleren henter værdierne:
String username = ctx.formParam("username");
String password = ctx.formParam("password");
Controlleren kalder servicen:
User user = userService.login(username, password);
Servicen kalder mapperen:
UserService
↓
UserMapper
↓
PostgreSQL
Mapperen finder brugeren.
Servicen kontrollerer loginoplysningerne og returnerer en User.
Controlleren gemmer brugeren i sessionen:
ctx.sessionAttribute("user", user);
Derefter redirecter controlleren:
ctx.redirect("/tasks");
Browseren laver nu en ny request:
GET /tasks
Controlleren henter brugeren fra sessionen:
User user = ctx.sessionAttribute("user");
Controlleren henter brugerens tasks gennem servicen:
List<Task> tasks = taskService.getTasksForUser(user);
Data sendes til Thymeleaf:
ctx.attribute("user", user);
ctx.attribute("tasks", tasks);
ctx.render("tasks.html");
Thymeleaf genererer HTML.
HTML’en sendes tilbage til browseren.
Hele forløbet:
Browser
│
│ POST /login
↓
Javalin
↓
UserController
↓
UserService
↓
UserMapper
↓
PostgreSQL
↑
UserMapper
↑
UserService
↑
UserController
↓
Thymeleaf
↓
HTML response
↓
Browser
Det er request/response-cyklussen i vores webapplikation.
Det kan være nyttigt at tænke på lagene sådan:
| Del | Ansvar |
|---|---|
| Browser | Sender requests og viser responses |
| Javalin | Modtager HTTP-requests og kobler dem til routes |
| Controller | Håndterer HTTP og koordinerer requesten |
| Service | Indeholder forretningslogik |
| Mapper | Kommunikerer med databasen |
| PostgreSQL | Gemmer data |
| Thymeleaf | Genererer HTML |
Det vigtigste er ikke at huske alle klasserne.
Det vigtigste er at kunne følge data gennem systemet:
Request
↓
Controller
↓
Service
↓
Mapper
↓
Database
Response
↑
Controller
↑
Service
↑
Thymeleaf
Når du arbejder med en fejl, kan du derfor spørge:
Hvor i request/response-cyklussen går det galt?