Routing

JWeb routing maps URLs to handlers. Handlers return Elements that are rendered as HTML, or Response objects for JSON/redirects.

Overview

Configure routes in a class implementing JWebRoutes. Routes support path parameters, query strings, and all HTTP methods.

import jweb.api.Component;

class HomePage implements Template {
    public Element render() { return div("Home"); }
}

@Component
public class Routes implements JWebRoutes {
    List<String> users = List.of("Ada", "Grace");

    public void configure(JWeb app) {
        // Page routes
        app.pages("/", HomePage.class);

        // API routes
        app.get("/api/users", req -> Response.json(users));
        app.post("/api/users", req -> createUser(req));
    }

    Object createUser(Request req) { return Response.json(201, req.body()); }
}

Page Routes

Register page classes that implement Template interface. A path may carry :param segments and a * wildcard.

import jweb.api.Component;

record User(String name) {}
class Users { User find(long id) { return new User("Guest"); } }
private Users users = new Users();
class HomePage implements Template { public Element render() { return div(); } }
class AboutPage implements Template { public Element render() { return div(); } }
class ContactPage implements Template { public Element render() { return div(); } }
class DocsPage implements Template { public Element render() { return div(); } }

@Component
public class Routes implements JWebRoutes {
    public void configure(JWeb app) {
        app.pages(
            "/", HomePage.class,
            "/about", AboutPage.class,
            "/contact", ContactPage.class,
            "/users/:id", UserPage.class,
            "/docs/*", DocsPage.class
        );
    }
}

// The page reads its params in beforeRender, or through pathParam()
public class UserPage implements Template {
    private User user;

    public void beforeRender(Request req) {
        user = users.find(req.requireParamLong("id"));
    }

    public Element render() {
        return div(h1(user.name()), p("Profile " + pathParam("id")));
    }
}

A page built from the request

A page whose constructor needs a query parameter, a store lookup or a flash message stays in the page table: register it with a lambda and it keeps the default layout, pageTitle(), styles(), scripts() and beforeRender(). Response.html(template) from a handler renders the same way, with the status and headers you set.

class AboutPage implements Template { public Element render() { return div(); } }
class SandboxPage implements Template {
    SandboxPage(String file) {}
    public Element render() { return div(); }
    public String pageTitle() { return "Sandbox - JWeb"; }
}

@Component
public class Routes implements JWebRoutes {
    public void configure(JWeb app) {
        app.pages("/sandbox", req -> new SandboxPage(req.query("file")));
        app.pages("/about", AboutPage::new);
    }
}

Page Template

Each page is a class implementing Template.

public class HomePage implements Template {
    public Element render() {
        return main(
            section(
                h1("Welcome to JWeb"),
                p("Build web apps in pure Java")
            )
        );
    }
}

public class AboutPage implements Template {
    public Element render() {
        return main(
            h1("About Us"),
            p("We build Java web frameworks")
        );
    }
}

Path Parameters

Capture dynamic segments from the URL path.

record User(long id) {}
class UserService { User findById(long id) { return new User(id); } }
UserService userService = new UserService();
Element userProfile(User user) { return div(); }
record Post(String slug) {}
class PostService { Post findBySlug(String category, String slug) { return new Post(slug); } }
PostService postService = new PostService();
Element postPage(Post post) { return div(); }
Element blogListing(int page) { return div(); }

// Single parameter
app.get("/users/:id", req -> {
    String id = req.param("id");
    User user = userService.findById(Long.parseLong(id));
    return userProfile(user);
});

// Multiple parameters
app.get("/posts/:category/:slug", req -> {
    String category = req.param("category");
    String slug = req.param("slug");
    Post post = postService.findBySlug(category, slug);
    return postPage(post);
});

// Optional with default
app.get("/blog/:page", req -> {
    int page = req.paramInt("page", 1);
    return blogListing(page);
});

Typed Parameters

Get parameters with automatic type conversion and validation.

class UserService { Object findById(int id) { return "user-" + id; } }
UserService userService = new UserService();
class OrderService { Object findById(long orderId) { return "order-" + orderId; } }
OrderService orderService = new OrderService();
class ProductService { Object findUnder(double maxPrice) { return List.of(); } }
ProductService productService = new ProductService();
class PostService { Object findByFeatured(boolean featured) { return List.of(); } }
PostService postService = new PostService();
class FileService { Object findById(UUID fileId) { return null; } }
FileService fileService = new FileService();

// Integer parameters
app.get("/users/:id", req -> {
    int id = req.paramInt("id");        // Throws if not a number
    return userService.findById(id);
});

// Long for large IDs
app.get("/orders/:orderId", req -> {
    long orderId = req.paramLong("orderId");
    return orderService.findById(orderId);
});

// Double for decimals
app.get("/products/:price", req -> {
    double maxPrice = req.paramDouble("price");
    return productService.findUnder(maxPrice);
});

// Boolean flags
app.get("/posts/:featured", req -> {
    boolean featured = req.paramBool("featured");
    return postService.findByFeatured(featured);
});

// UUID for unique identifiers
app.get("/files/:fileId", req -> {
    UUID fileId = req.paramUUID("fileId");
    return fileService.findById(fileId);
});

Query Parameters

Access query string parameters from the URL.

record Result(String title) {}
class SearchService { List<Result> search(String q, int page, String sort) { return List.of(); } }
SearchService searchService = new SearchService();
Element searchResults(List<Result> results, String query, int page) { return div(); }
Element productListing(String category, boolean inStock) { return div(); }

// GET /search?q=java&page=2&sort=date
app.get("/search", req -> {
    String query = req.query("q");            // null when absent
    int page = req.queryInt("page", 1);       // with default
    String sort = req.query("sort") != null ? req.query("sort") : "relevance";

    List<Result> results = searchService.search(query, page, sort);
    return searchResults(results, query, page);
});

// Check if parameter exists
app.get("/products", req -> {
    boolean inStock = req.query("inStock") != null;
    String category = req.query("category") != null ? req.query("category") : "all";
    return productListing(category, inStock);
});

Typed Routes & Query Params

Declare a route's path and parameter types once — registration and every link to it are then compile-time checked. Handlers receive already-parsed values; bad input returns 400, not 500.

Element userPage(Long id) { return div("User " + id); }
Element productList(int page) { return div("Page " + page); }

static final TypedRoute.Path1<Long> USER =
    TypedRoute.path("/users/:id", Long.class);

app.get(USER, (req, id) -> userPage(id));   // id is a Long
a(href(USER.url(42L)), "Profile")           // "/users/42"

// Query params: parsed, defaulted, no null checks
static final QueryParam<Integer> PAGE = QueryParam.of("page", Integer.class).orElse(1);
app.get("/products", req -> productList(PAGE.from(req)));
Supported param types: String, Integer, Long, Double, Boolean, UUID. Two-param routes use TypedRoute.path(pattern, A.class, B.class).

Action Routes

A mutation reachable from a link or a form: the query/form parameters bind to a record before the handler runs. Enums bind by name, numbers parse, Optional<T> marks an optional parameter, @Range/@Length/@Pattern validate. Anything missing or invalid is a 400 with a message — never an exception page.

import jweb.api.Range;

enum Setting { EBB, SLACK, FLOOD }
record Vane(@Range(min = 1, max = 3) int n, Setting set) {}
record Pref(Optional<Setting> sound, Optional<Boolean> motion) {}

// GET /act/tide/vane?n=2&set=flood  — or a POSTed form with the same fields
app.action("/act/tide/vane", Vane.class, (req, vane) -> {
    Visit v = Session.of(Visit.class, req);
    v.vanes.put(vane.n(), vane.set());
    return Response.redirect("/worlds/tide-archive")
        .anchor(v.vanesAligned() ? "content" : "current-chamber");
});

app.action("/act/pref", Pref.class, (req, pref) -> {
    Visit v = Session.of(Visit.class, req);
    pref.sound().ifPresent(s -> v.soundOn = s != Setting.EBB);
    return Response.redirectBack(req);           // the Referer, or "/"
});

// One method only
app.get("/messages", View.class, (req, view) -> ...);
app.post("/login", Login.class, (req, login) -> ...);

// Anywhere else: bind by hand (BindException → 400 if it escapes)
Search search = req.bind(Search.class);

Session

jweb.Session is the visitor's session: typed, null-safe, with one-shot flash messages. A page reaches it without a Request through session().

import jweb.Session;

class Visit { String name = "Guest"; }

Visit visit = Session.of(Visit.class, req);        // created on first use, shared after
Session session = Session.of(req);
session.put("theme", "dark");
String theme = session.get("theme", String.class, "light");
session.flash("notice", "Saved!");                // read once, then gone
String notice = session.flash("notice");
session.end();                                    // what Auth.logout does

public class AccountPage implements Template {
    public Element render() {
        Visit visit = session().of(Visit.class);   // the request in flight
        return p("Welcome back, " + visit.name);
    }
}
The session is the container's: created on the first write, 30 minutes idle by default, kept in server memory — small, serializable values only.

HTTP Methods

Handle different HTTP methods with dedicated methods.

class User { long getId() { return 1; } }
class UserService {
    User save(String name, String email) { return new User(); }
    void update(long id, String json) {}
    void delete(long id) {}
}
Element userList() { return div(); }
Element userDetail(String id) { return div(); }
UserService userService = new UserService();

// GET - retrieve data
app.get("/users", () -> userList());
app.get("/users/:id", req -> userDetail(req.param("id")));

// POST - create new resource
app.post("/users", req -> {
    User saved = userService.save(req.formParam("name"), req.formParam("email"));
    return Response.redirect("/users/" + saved.getId());
});

// PUT - update entire resource
app.put("/users/:id", req -> {
    Long id = req.paramLong("id");
    String json = req.body();  // raw request body
    userService.update(id, json);
    return Response.noContent();
});

// DELETE - remove resource
app.delete("/users/:id", req -> {
    userService.delete(req.paramLong("id"));
    return Response.noContent();
});

Response Types

Return different response types from handlers.

class UserService { List<Object> findAll() { return List.of(); } }
UserService userService = new UserService();
boolean authenticate(Request r) { return true; }
boolean saved = true;
Element maintenancePage() { return div(); }
Object item = new Object();

// HTML response (default)
app.get("/page", () -> div("HTML content"));

// JSON response
app.get("/api/users", req -> Response.json(userService.findAll()));

// Redirect
app.get("/old-page", req -> Response.redirect("/new-page"));
app.post("/login", req -> {
    if (authenticate(req)) {
        return Response.redirect("/dashboard");
    }
    return Response.redirect("/login?error=true");
});
app.get("/act/save", req -> Response.redirect("/about").anchor(saved ? "done" : "form"));
app.get("/act/toggle", req -> Response.redirectBack(req));    // the Referer, or "/"

// Status codes — no HttpStatus import, ever
app.get("/not-found", req -> Response.notFound());
app.get("/forbidden", req -> Response.forbidden());
app.get("/error", req -> Response.serverError());
app.get("/limited", req -> Response.tooManyRequests());
app.get("/deleted", req -> Response.noContent());
app.get("/bad", req -> Response.badRequest("Invalid email"));
app.get("/teapot", req -> Response.error(418, "I'm a teapot"));
app.get("/down", req -> Response.html(503, maintenancePage()));
app.get("/made", req -> Response.json(201, item));

Custom Responses

String rss = "<rss></rss>";
String etag = "abc123";
class FileService { byte[] read(String file) { return new byte[0]; } }
FileService fileService = new FileService();

// Custom headers
app.get("/custom", req -> Response.ok()
    .header("X-Custom-Header", "value")
    .body("Hello"));

// Any status, any content type
app.get("/feed", req -> Response.status(200)
    .contentType("application/rss+xml; charset=UTF-8")
    .body(rss));
app.get("/cached", req -> Response.status(304).header("ETag", etag).build());

// 201 Created with Location header
app.post("/items", req -> Response.created("/items/42"));

// File download
app.get("/download/:file", req -> {
    byte[] data = fileService.read(req.param("file"));
    return Response.ok()
        .header("Content-Disposition", "attachment; filename=report.pdf")
        .body(data);
});

Guards

A guard runs before every route under a path pattern — page routes, router routes and @REST controllers alike. It returns null to let the request through, or the response to send instead.

// Your own check: req.principal() is what Auth.login stored
app.guard("/admin/**", req -> req.principal() != null
    ? null
    : Response.redirect("/admin/login"));

// Sugar: redirect anonymous visitors, let the login page itself through
app.guard("/admin/**", Auth.requireLogin("/admin/login"));

// Any Middleware works as a guard — its own response (or 401/403) answers
app.guard("/admin/**", Auth.requireRole("admin"));
app.guard("/api/v1/admin/**", Jwt.protect());
  • Patterns are the app.use(...) ones: a prefix (/admin), /admin/** for the prefix and everything under it, or a glob with * inside one segment.
  • Guards run after the middleware stack and before the handler, in registration order; the first non-null answer wins.
  • The same guard covers @REST controllers under the prefix — middleware still does not.
Guards decide who reaches a route. What a server event handler may do is decided when the page renders — render nothing for those who may not act.

Middleware

Add middleware for authentication, logging, etc.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Logger logger = LoggerFactory.getLogger(getClass());

// Apply to all routes
app.use((req, chain) -> {
    long start = System.currentTimeMillis();
    Object response = chain.next();
    long duration = System.currentTimeMillis() - start;
    logger.info("{} {} - {}ms", req.method(), req.path(), duration);
    return response;
});

// Apply to specific path prefixes
app.use("/admin", Auth.requireAuth("/login"));
app.use("/api", Jwt.protect());

// Multiple middleware (one call each)
app.use("/api", RateLimit.perMinute(100).byIp().build());
app.use("/api", Cors.allowAll());

Custom Middleware

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// A named, reusable Middleware
class LoggingMiddleware implements Middleware {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public Object handle(Request req, MiddlewareChain chain) throws Exception {
        logger.info("Request: {} {}", req.method(), req.path());
        try {
            Object response = chain.next();
            logger.info("Response: 200 OK");
            return response;
        } catch (Exception e) {
            logger.error("Error: {}", e.getMessage());
            throw e;
        }
    }
}

// Register
app.use(new LoggingMiddleware());