Routing
A route is a method, a path, the function that answers it, and who may call it. Each feature of an app keeps its routes in one file, routes.zig.
The code on this page is from Posts, the example app in examples/posts (source). These are all its routes:
/// Routes of the posts feature. src/main.zig mounts them with
/// `server.mountFeatures(features)`: every pub fn here with no parameters
/// that returns a spider.Group. Paths are relative to the group prefix.
///
/// Reading is for everyone: `defaults` makes the routes public. Writing
/// needs a login: those routes say `.authenticated`, which replaces the
/// default for them. Changing or deleting a post is for its author: those
/// routes name a rule of src/features/posts/policy.zig, which also needs
/// a login.
pub fn build() spider.Group {
const signed_in = .{ .authenticated = true };
var g = spider.Group.init("/posts");
_ = g
.defaults(.{ .public = true })
.get("", controller.index, .{})
.get("/export.csv", controller.exportCsv, .{})
.get("/new", controller.new, signed_in)
.post("/create", controller.create, signed_in)
.get("/:id", controller.show, .{})
.get("/:id/edit", controller.edit, .{ .policy = Posts.route(.update) })
.post("/:id/update", controller.update, .{ .policy = Posts.route(.update) })
.get("/:id/delete", controller.confirmDelete, .{ .policy = Posts.route(.delete) })
.post("/:id/delete", controller.delete, .{ .policy = Posts.route(.delete) });
return g;
}
build returns a group: routes that share a prefix. Each path is added to the prefix, so "" is /posts itself and /new is /posts/new.
Every route takes three arguments: the path, the handler, and a config that says who may call it. defaults says it once for the whole group: .public, anyone. A route that names its own access replaces the default, and here the routes that write do: .authenticated is anyone who is signed in, and .policy names a rule: here, that a post is changed by who wrote it. A route with .{} keeps the default. Login and Access policies explain the rest.
Dynamic segments
A segment that starts with a colon matches any value at that position: /:id answers /posts/4 and /posts/9. The handler reads the value by its name with c.params.get. The value arrives decoded: /tags/caf%C3%A9 gives café.
Posts looks up a post in one place:
/// The post named by the :id of the path. Null (no such post, or an id
/// that is not a number) is the 404 page.
pub fn load(c: *spider.Ctx) !?model.Post {
const text = c.params.get("id") orelse return null;
const id = std.fmt.parseInt(i64, text, 10) catch return null;
return repository.find(c.arena, id);
}
The value is text. load turns it into a number, and gives null when it is not one or when no post has that id. The page of a post answers that with 404, by returning error.NotFound:
pub fn show(c: *spider.Ctx) !Response {
const post = (try policy.load(c)) orelse return error.NotFound;
return c.view("posts/show", .{
.title = post.title,
.post = try present.card(c.arena, post),
// The same rule the routes use decides whether the buttons show.
.can_edit = try Posts.can(c, .update, &post),
// The body is Markdown. Spider's renderer escapes what the author
// typed, so its HTML can go into the page as it is.
.body = spider.RawHtml{ .html = try spider.zmd.parse(c.arena, post.body, .{}) },
}, .{});
}
A fixed path wins over a dynamic one: /posts/new reaches new, not show with an id of "new". The order the routes are written in does not matter.
How routes reach the server
src/main.zig does not list routes. It mounts the features, and that registers every function of a feature's routes.zig that takes no parameters and returns a group:
/// Builds the server and listens. main() calls it, and so do the tests in
/// src/app_test.zig.
pub fn serve(allocator: std.mem.Allocator, io: std.Io) !void {
// What the server needs before it starts goes here. Posts needs
// nothing yet.
_ = allocator;
_ = io;
var server = spider.app(.{});
defer server.deinit();
// Routes live with their feature (src/features/<name>/routes.zig);
// mountFeatures registers every feature listed in src/features/mod.zig.
// `spider routes` prints what got registered.
try server
.use(spider.logger)
// Reads the login cookie (or a bearer token) and tells the routes
// who is there; a route that is not `.public` needs a login.
.use(spider.session.middleware())
.mountFeatures(features)
.onError(onError)
// Port and host come from spider.config.zig; the PORT variable
// (the environment or .env) replaces the port.
.listen(.{});
}
So a feature can have more than one group. The posts feature has three: the pages above, a JSON API under /api/posts, and one outside both for the address of the site itself:
/// The address of the site itself goes to the feed.
pub fn home() spider.Group {
var g = spider.Group.init("");
_ = g.get("/", controller.home, .{ .public = true });
return g;
}
See what is registered
spider routes
It prints every route of the app with who may call it:
GET / public
GET /_spider/health public quiet_log
GET /api/posts public
POST /api/posts public
GET /api/posts/:id public
GET /auth/account authenticated
GET /auth/login public
POST /auth/login public
POST /auth/logout authenticated
GET /auth/register public
POST /auth/register public
GET /posts public
GET /posts/:id public
GET /posts/:id/delete authenticated
POST /posts/:id/delete authenticated
GET /posts/:id/edit authenticated
POST /posts/:id/update authenticated
POST /posts/create authenticated
GET /posts/export.csv public
GET /posts/new authenticated
GET /up public quiet_log
21 routes
/up and /_spider/health come with Spider: they answer whether the app is running.