Controllers and apiBuilder
When all handlers are lambdas in main, the method quickly grows to a hundred lines. The usual practice is to move handlers into a separate class — a controller. Each of its methods takes a Context and works as a handler. Routes stay in Main, and to group them Javalin has the apiBuilder — a set of static methods path, get, post, delete.
The controller class
Create the post controller file. It receives the store through its constructor:
// src/main/java/com/example/blog/PostController.java
package com.example.blog;
import io.javalin.http.Context;
import io.javalin.http.HttpStatus;
import io.javalin.http.NotFoundResponse;
public class PostController {
private final PostStore store;
public PostController(PostStore store) {
this.store = store;
}
public void getAll(Context ctx) {
ctx.json(store.findAll());
}
public void getOne(Context ctx) {
long id = ctx.pathParamAsClass("id", Long.class).get();
Post post = store.findById(id)
.orElseThrow(() -> new NotFoundResponse("Post " + id + " not found"));
ctx.json(post);
}
public void create(Context ctx) {
NewPost data = ctx.bodyValidator(NewPost.class)
.check(p -> p.title() != null && !p.title().isBlank(), "Title is required")
.check(p -> p.body() != null && p.body().length() >= 5, "Body is too short")
.get();
ctx.status(HttpStatus.CREATED).json(store.create(data));
}
public void delete(Context ctx) {
long id = ctx.pathParamAsClass("id", Long.class).get();
if (!store.delete(id)) {
throw new NotFoundResponse("Post " + id + " not found");
}
ctx.status(HttpStatus.NO_CONTENT);
}
}The method code is the same as in the lambdas, but now each piece has a name.
Routes with apiBuilder
In Main.java create the controller and replace the post routes with an apiBuilder block:
// src/main/java/com/example/blog/Main.java
package com.example.blog;
import io.javalin.Javalin;
import static io.javalin.apibuilder.ApiBuilder.delete;
import static io.javalin.apibuilder.ApiBuilder.get;
import static io.javalin.apibuilder.ApiBuilder.path;
import static io.javalin.apibuilder.ApiBuilder.post;
public class Main {
public static void main(String[] args) {
var posts = new PostController(new PostStore());
Javalin app = Javalin.create(config -> {
config.routes.apiBuilder(() -> {
path("/posts", () -> {
get(posts::getAll);
post(posts::create);
path("/{id}", () -> {
get(posts::getOne);
delete(posts::delete);
});
});
});
// the before, after and exception handlers from earlier lessons stay here
});
app.start(8080);
}
}path sets the shared part of the address, nested path calls continue it. posts::getAll is a reference to a method of the specific posts object. The structure of the block shows all API addresses at a glance.
| Call inside the block | Resulting route |
|---|---|
get(posts::getAll) in path("/posts") | GET /posts |
post(posts::create) in path("/posts") | POST /posts |
get(posts::getOne) in path("/{id}") | GET /posts/{id} |
delete(posts::delete) in path("/{id}") | DELETE /posts/{id} |
The API works as before:
curl -X POST http://localhost:8080/posts \
-d '{"title":"Controllers","body":"Code moved to a class.","author":"Oleg"}'{"id":3,"title":"Controllers","body":"Code moved to a class.","author":"Oleg"}
Common mistake
The static get, post, path methods from ApiBuilder work only inside config.routes.apiBuilder(...). If you call get("/", ...) directly in the configuration block, the server fails at startup:
Exception in thread "main" java.lang.IllegalStateException: The static API can only be used within a routes() call.
Inside Javalin.create write config.routes.get(...), and use the short get(...) only inside apiBuilder.