Server routes
Add a route to the frontend server when the work belongs next to the browser's session: a sign-in callback, a cookie update or a server-side request to the backend. For ordinary business data, keep the route in the backend and use the proxy.
Return your first response
In a frontend module already registered with the backend, add this file beside dms.frontend.ts:
import { defineDmsServer } from "#dms/server";
export default defineDmsServer((server) => {
server.registerRoute("GET", "/api/_acme/ping", () => ({ ok: true }));
});
Run ajs dms dev and request the route on the frontend's printed Local URL, not the backend port:
curl http://localhost:3001/api/_acme/ping
The response is HTTP 200 with {"ok":true}. This route needs no database or session and returns no private data. A handler is not automatically authenticated: check the session and authorize access before returning private data.
Forward an authenticated action
Once the ping works, move handlers into server/ when they need more code. This example assumes the backend already serves /api/acme/exports/:kind and checks permission to export:
import { defineDmsServer } from "#dms/server";
import { ping, startExport } from "./server/routes";
export default defineDmsServer((server) => {
server.registerRoute("GET", "/api/_acme/ping", ping);
server.registerRoute("POST", "/api/_acme/exports/:kind", startExport);
});
import type { DmsServerEvent } from "#dms/server";
export function ping() {
return { ok: true };
}
export async function startExport(event: DmsServerEvent) {
const input = await event.readJson<{ from: string }>();
return event.backend(`/api/acme/exports/${event.params.kind}`, {
method: "POST",
body: input,
token: event.session.read()?.accessToken,
});
}
Send the POST from the dashboard's own origin; the frontend rejects a state-changing request with no matching Origin by default. The backend must still authenticate and authorize the forwarded request. The same server object also registers auth hooks, route rules and backend proxies.
The handler
A handler takes the request and answers with a Web-standard Response, or with a value sent as JSON with a 200. null or nothing is an empty 204. Cookies written through the event are added to the response either way.
The event it receives carries:
| Member | What it gives |
|---|---|
request, url | The request, Web-standard. Its body stops at the route's body limit. |
params | The :name segments, decoded |
readJson() | The body as JSON, {} when empty; a 400 when it is not JSON |
session | read(), view() (the page props the session shows), write() (stamped with updatedAt; ends with the browser unless persistent), clear(), useForRequest() |
cookies | Sealed cookies by logical name: readSealed, writeSealed (optional maxAge and path), clear |
backend(path, init) | A JSON call to the backend that carries the client's address and the headers the proxy relays. A refusal throws an error with the status and the backend's public error.* message. |
backendUrl(path) | The backend URL of a path, always on the backend's origin, for a call backend cannot make, such as a raw response |
clientIp() | The client's address, behind DMS_TRUSTED_PROXY_HOPS proxies |
renderEmail(template, props, { locale }) | An e-mail template of the application, rendered to HTML |
module | This module's id, its options and its own privateOptions, never another module's |
app.authEstablishEndpoints | The backend endpoints declared as session-opening |
Sealed cookies
The server scopes a cookie's name to its backend and sets HttpOnly, SameSite=Lax and Secure, so a module never depends on how cookies are named. The session cookie is only reachable through session. The dms_* names belong to the module that owns sign-in, the one whose registerAuth is used: another module naming one gets an error, and its own cookies are stored as <module id>.<name>, so no module reads or overwrites another's.
Each character of the module id outside A-Za-z0-9_- becomes - in that cookie namespace. A cookie previously written under its bare name is not read through the module's scoped name.
Defaults every route gets
- A request with a state-changing method must carry the frontend's own
Origin. Pass{ sameOrigin: false }only for a route authenticated another way, such as by a service token. - The body stops at 64 KiB (
bodyLimit), answered with a413. - The answer is
Cache-Control: no-storeunless the handler sets its own. - An error the handler lets through is answered like a backend failure: its status, and only a public
error.*message.
Paths and precedence
Paths are literal segments and :name parameters. Module routes are tried after the renderer's own paths (/api/_dms/, /@…, /assets/, which no module can declare) and before the backend proxies, literal paths before parameters. A route owns its path for every method: a method it does not declare answers 405, and a HEAD reaches the GET route.
- A route under
/api/or/dms/outside a module namespace (/api/_<module>/) hides what the backend serves there, so the server warns unless the route passes{ overridesBackend: true }. - When two modules declare the same method and path, the module of higher priority keeps it and a warning names both; the module id settles a tie.
- A module route that answers a public endpoint is reported too.
- A malformed declaration is ignored with a warning naming its module.
Trust and imports
dms.server.ts runs in the frontend server's Node.js process, next to the session secret. It is trusted code, as the module's backend half is, not a sandbox: every route can read the session, access and refresh tokens included, and write it. Only install a module whose dms.server.ts you trust as much as the DMS's own.
It may import #dms/server, Node.js built-ins, npm dependencies and the module's own server files, which is why a module's server/ directory is copied into the workspace. It never imports the application's code (.vue files, dms.frontend.ts), and nothing it imports reaches the browser bundle; verify-source checks both. Auto-imports do not apply to server code: import what a handler uses.
Under ajs dms dev the server loads the file through Vite, so an edited handler answers on the next request. ajs dms build bundles it into dist/server-modules; a workspace built without that bundle serves no module route.
dms.server.ts raises its supported range floor to the release that serves it: an older loader never reads the file, and the routes would silently be missing.Testing routes
serveModuleRoutes, from @antelopejs/dms-frontend/templates/vue/server/module-testing.mjs, serves the default export of a module's dms.server.ts on a local port, through the same router, event and cookie jar as the frontend server. See Testing.
Auto-imports
Declare which directories of a module are auto-imported across the application, in dms.frontend.build.ts.
Sessions and sign-in
The auth hooks the module that owns sign-in registers, how page props and the interface language follow the session, and how a module flow opens a session through /auth/establish.