[data-reveal]{opacity:1!important;transform:none!important}
Server routes
Frontend

Server routes

Serve routes from the frontend server in dms.server.ts, next to the session, with the event API, the defaults every route gets, and how to test them.

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:

dms.server.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:

Terminal — replace the port if needed
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:

dms.server.ts
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);
});
server/routes.ts
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:

MemberWhat it gives
request, urlThe request, Web-standard. Its body stops at the route's body limit.
paramsThe :name segments, decoded
readJson()The body as JSON, {} when empty; a 400 when it is not JSON
sessionread(), view() (the page props the session shows), write() (stamped with updatedAt; ends with the browser unless persistent), clear(), useForRequest()
cookiesSealed 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
moduleThis module's id, its options and its own privateOptions, never another module's
app.authEstablishEndpointsThe 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 a 413.
  • The answer is Cache-Control: no-store unless 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.

A module that ships 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.