# Router Routing in Elements maps each HTTP URL to a function handler that responds to requests at that url. Routes are declared explicitly with `app.route()` in the project's root `index.ts`. The `app/pages/` folder is a convention for storing page templates and route logic, and folder names do not affect routing at all. The router works equally well for serving HTML pages and serving API endpoints. URL patterns are full `URLPattern` expressions, the web standard for URL pattern matching. Patterns support `:name` segments, `*` wildcards, optional groups, and inline regex like `:id(\d+)`. The second argument to `app.route()` is either a handler function or a child router. Routes are the entry point for HTML pages. A typical route handler will gather the data the page needs and return a new HTML template whose parameters are provided by the route function. Elements server-renders the HTML, sends the page along with any live tables, channels, and other reactive values it carries over the wire, and attaches it in the browser. Route handlers have the same data features as `@rpc` functions. The `session` global is available with the current user. Return an HTML template and Elements server-renders it. Return an object or array and Elements serializes it through the extended JSON serializer, preserving class identity, handling object cycles, and serializing live tables. Return a string and it goes out as `text/plain`. Write to `res` directly and return nothing. `app.use()` registers a handler that runs before any route matches. Child routers can define their own `use()` handlers. This is where cross-cutting concerns like logging, custom headers, and feature flags go. Body parsing, sessions, asset serving, and form handling are built in. `app.route()` and `app.use()` register against the app's default router. Larger apps create child routers with `new Router()` and mount them at a path prefix like `/admin/*`. Routers nest as deep as needed. Routing runs on the server, and the browser attaches to whatever the route renders. ## At a Glance ```ts // index.ts import { App } from "@elements/app"; import config from "#config"; import home from "#app/pages/home"; import roomsShow from "#app/pages/rooms-show"; import upload from "#app/pages/upload"; import api from "#app/api"; const app = new App(); app.use((req, res, next) => { console.log(`${req.method} ${req.url}`); next(); }); app.route("/", home); app.route("/rooms/:id", roomsShow); app.route({ method: "post", path: "/upload", handler: upload, }); app.route("/api/*", api); app.error((req, res, err) => { res.status(err.statusCode ?? 500).send(`error: ${err.message}`); }); app.start(config); ``` ## Handlers A handler is a function or an object with a `handle()` method. ```ts Type signature type Handler = HandlerFunction | HandlerObject; type HandlerFunction = (req: Request, res: Response) => HandlerResult | Promise; type HandlerObject = { handle(req: Request, res: Response): HandlerResult | Promise; mount?: boolean; }; ``` A page's `index.ts` exports a route handler as the default export: ```ts // app/pages/home/index.ts import { Request, Response } from "@elements/app"; import html from "./template"; export default function route(req: Request, res: Response) { let users = sql(`select * from users`).all(); return new html({ users }); } ``` ## Return Values Whatever a handler returns becomes the response. - A `Sendable` (for example, an HTML template instance) is sent with ETag and gzip. - A `string` is sent as `text/plain`. - An object or array is serialized as Elements JSON and sent. - `void` or `undefined` means the handler used `res` directly. ```ts index.ts app.route("/api/rooms", () => sql(` select id, name from rooms order by createdAt desc `).all()); app.route("/health", (req, res) => "ok"); app.route("/redirect", () => { redirect("/"); }); ``` ## URL Patterns Routes are `URLPattern` expressions. Dynamic segments use `:name`, wildcards use `*`, optional groups use `?`, and inline regex goes inside parentheses. ```ts index.ts app.route("/", home); app.route("/rooms/:id", roomsShow); app.route("/posts/:id/comments/:commentId", showComment); app.route("/files/*", fileServer); app.route("/users/:id(\\d+)", showUser); app.route("/archive/:year/:month?", archive); ``` Match parameters land in `req.params`: ```ts Example import { Request, Response } from "@elements/app"; function route(req: Request, res: Response) { let id = req.params.id; let cid = req.params.commentId; } ``` Folder names under `app/pages/` do not affect routing. The route URL is whatever you pass to `app.route()`. ## Method Matching A route matches any method by default. ```ts index.ts app.route("/api/users", listUsers); // any method app.route({ // POST only method: "post", path: "/api/users", handler: createUser, }); app.route({ // GET or POST method: ["get", "post"], path: "/api/data", handler: data, }); ``` ## Domain Matching Restrict a route to a specific hostname. ```ts index.ts app.route({ domain: "api.example.com", path: "/v1/*", handler: apiRouter, }); ``` ## `use()` Handlers `app.use()` registers a handler that runs before any route on the router matches. Call `next()` to continue, send a response to stop the chain, or throw to trigger the error handler. Handlers run in registration order. ```ts index.ts app.use((req, res, next) => { req.startTime = Date.now(); next(); }); ``` Each router has its own `use()`. A handler registered on a child router only runs for that router's routes. ```ts index.ts import { Router } from "@elements/app"; const api = new Router(); api.use((req, res, next) => { res.setHeader("Cache-Control", "no-store"); next(); }); api.route("/users", listUsers); app.route("/api/*", api); ``` Body parsing, session loading, asset serving, and form handling are built in. Use `use()` for your own cross-cutting concerns: request logging, custom headers, feature flags. ## Sub-Routers Nested routers mount at a path prefix. ```ts index.ts import { Router } from "@elements/app"; const api = new Router(); api.route("/users", listUsers); api.route("/users/:id", getUser); app.route("/api/*", api); ``` Inside `api`, the URL has the `/api` prefix stripped (`/users`, not `/api/users`). The `Router` class has `mount: true` by default. The mount also answers its own root: `/api` reaches the route `api` registers at `"/"`. A path under the mount that `api` has no route for is a 404 from the app, and redirects keep the prefix, so `/api/Users` goes to `/api/users`. ## Error Handler ```ts index.ts app.error((req, res, err) => { switch (err.statusCode) { case 404: return notFound(req, res, err); default: return unhandled(req, res, err); } }); ``` The error handler runs when a route handler throws or when no route matches. Throwable error types from `@elements/app`: - `AuthError` (401): unauthenticated. - `ForbiddenError` (403): authenticated but not permitted. - `NotFoundError` (404): resource missing. - `ValidationError` (422): invalid input. - `NotAcceptableError` (406): request body unparseable. - `PartitionMismatchError` (400): a LiveTable row outside the view's partition. Throw them directly when something is wrong. For a missing row, `firstOrThrow` throws the `NotFoundError` for you, with your message: ```ts Example let room = sql(` select * from rooms where id = ${req.params.id} `).firstOrThrow(`room ${req.params.id} not found`); ``` Internal or unsafe errors are wrapped in `ServerError` before reaching the client. ## First-Match-Wins Routes match in registration order. The first route that matches wins. Order specific routes before wildcards: ```ts index.ts // correct app.route("/api/users", listUsers); app.route("/api/*", api); // wrong: api/* catches /api/users first app.route("/api/*", api); app.route("/api/users", listUsers); ``` ## Canonical URLs Each page has one URL. A request that differs from a route only in its trailing slash or its case gets a 301 to the canonical form, so search engines see one page, not two. By default the canonical form has no trailing slash: `/about/` redirects to `/about`. Set `server.trailingSlash` in `config.jsoc` to `"always"` to make the slash form canonical instead, so `/about` redirects to `/about/`. Use it when a site you are replacing already ranks with slash URLs: ```jsonc config.jsoc server: { trailingSlash: "always", // or "never", the default }, ``` Routes are written without the slash either way (`app.route("/about", ...)`), and both forms reach the same handler. The root `/` and a path ending in a file name, such as `/feed.xml`, never take a slash. Only `GET` and `HEAD` are redirected: a `POST` to `/save/` runs the `/save` handler, because a 301 would ask the client to repeat the write somewhere else. ## Redirecting `redirect(url)` sends the visitor somewhere else. It is the same call templates use, so a guard reads identically in a route, in a `use()` handler, and in a template event handler. ```ts app/pages/dashboard/index.ts import { Request, Response, redirect, session } from "@elements/app"; export default function route(req: Request, res: Response) { if (!session.isLoggedIn()) { redirect("/signin"); return; } return new dashboard(); } ``` It writes a 302 and ends the response. It is not control flow: the handler keeps running, and a value returned afterward is discarded rather than written over the redirect, so both the early-return shape above and a plain fall-through work. Return early when you want the rest of the handler skipped. For a status other than 302, use `res.redirect(url, 301)`. ## The Request and Response `req` and `res` are enhanced Node `IncomingMessage` and response objects. ```ts Request and response req.method req.url // current routing-context URL (sub-routers may mount-strip a prefix) req.originalUrl // original URL, never modified by routers req.parsedUrl // URL object for the current routing context req.params // merged: query < body < path. path wins. req.path // path-only params from the URLPattern match req.query // query-string only (string | string[] values) req.body // parsed body (json, form-data with File instances, urlencoded) req.bodyBuffer // raw body buffer res.status(404) res.send(value) // any sendable: string, object, html instance res.redirect("/", 301) // status other than 302; otherwise use redirect(url) res.end() ``` Access the current session via the global `session` namespace: ```ts index.ts import { session, AuthError } from "@elements/app"; app.route("/me", (req, res) => { if (!session.isLoggedIn()) { throw new AuthError(); } return sql(` select * from users where id = ${session.getOrThrow("userId")} `).firstOrThrow("not found"); }); ``` ## Related - `start`: the `App` instance, `app.start()`, application structure. - `rpc`: the `@rpc` boundary, authorization, error catalog. - `session`: reactive session, login/logout, sliding window. - `html`: HTML templates and the route-to-page flow.