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)));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);
}
}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.
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());