Reference

API reference

Every public export from @devorajs/core, grouped by what it's for. This is the code-level companion to Core concepts and Backend — those pages explain the ideas, this page is what to actually import.

Where things come from

Three entry points, not one — importing from the wrong one is the most common mistake:

Import pathUse it fromHas
@devorajs/coreAny ssr/ssg/isr/streaming route or server fileEverything below except the client-only row
@devorajs/core/clientA csr route, or any island componentPageShell, AppHeader, theme — browser-safe subset only
@devorajs/core/configdevora.config.ts / app.config.tsdefineProject, defineApp

Why the split exists

The main entry's export chain reaches real Node builtins (node:crypto in session/CSRF code, node:fs in the router) — fine for a server file, but a renderMode: "csr" route is also built for the browser. Importing PageShell from the main barrel inside a csr route breaks that client build outright. Use @devorajs/core/client there instead — see the csr example.

Page & navigation

PageShell — wraps a route's content with the shared header + content container every app uses, so a page looks the same whether it's marketing, dashboard, or admin.

function PageShell(props: {
  appName?: string;   // shown as a badge in the header — omit for e.g. marketing
  nav?: NavLink[];     // this app's own link set (see apps/*/nav.ts)
  footer?: ReactNode;  // renders inside the shared <footer>, above "Built with devora.js"
  footerLinks?: NavLink[]; // plain links in the footer's bottom row (since 0.3.2)
  children: ReactNode;
}): JSX.Element

NavLink — the shape nav expects: { label: string; href: string }. AppHeader is the header alone (what PageShell renders internally) — reach for it directly only if you're composing your own page chrome instead of using PageShell.

Since 0.3.2 the framework's stylesheet and favicon are only added to pages that render PageShell or AppHeader; head.theme overrides that. The shell's markup is a .devora-shell column (header, main.devora-page, footer), so the footer stays at the bottom of short pages. Its CSS custom properties (--devora-bg, --devora-surface, --devora-fg, --devora-accent-from, ...) can be reused in your own CSS.

Config

defineApp(config) — apps/<name>/app.config.ts's default export. Identity wrapper for type inference; every field is optional.

FieldTypeDoes
defaultRenderModeRenderModeApplies to any route in this app with no renderMode export of its own
security.cspstringOverrides the default Content-Security-Policy for this app — see Security model
security.hstsbooleanToggles the HSTS header
security.frameOptions"DENY" | "SAMEORIGIN"Overrides X-Frame-Options
security.noindexbooleanSends X-Robots-Tag: noindex on every response (an admin panel, an internal tool)
sitemapbooleanServes /sitemap.xml for this app. Opt-in, default false
backendOnlybooleanPure-API app — skips the client build and SSR entry entirely. See Backend
apiDirstringFolder for API route files, served under /api. Default "api"; new apps use "endpoints"
headHeadConfig{ lang?, stylesheets?, links?, meta?, scripts?, theme? } for every page — see Pages, forms and errors
maxBodyBytesnumberLargest request body for forms and API routes; larger gets a 413. Default 10 MB
corsCorsConfig{ origins, credentials?, methods?, allowHeaders?, exposeHeaders?, maxAge? } for API routes. Off by default — see Backend
trustProxyboolean | numberTrust X-Forwarded-* from that many proxies, for ctx.url, ctx.ip and secure: "auto". Default false for devora start, true in the Vercel/Netlify functions
csrfbooleanfalse: never issue the CSRF cookie (Bearer-only APIs); verifyCsrf() then passes only for Bearer-authenticated requests
sessionsAppSessionsConfigPer-app cookie overrides; activeSeconds/idleSeconds for auth: "isolated" apps
prebuildstring | string[]Shell commands devora build/deploy run in the app folder first (codegen, a CSS build); a non-zero exit stops the build
vercel.runtimestringVercel function runtime, e.g. "nodejs22.x"; picked from Node/engines when unset

defineProject(config) — the project-root devora.config.ts's default export: { apps: AppConfig[], shared: SharedConfig }. Each AppConfig is { name, dir, domain, auth?, devPort? }; SharedConfig is { core, backend?, auth, sessions? } (the project-wide default auth, overridable per app; no backend in a frontend-only project). sessions is { store?, activeSeconds?, idleSeconds?, cookie? } — where session records live (a module path or "memory"), how long they last, and the cookie's { secure?: true | "auto" | false, sameSite?: "lax" | "strict" | "none", domain? }; a store is required in production for any app with login, see Security model. AuthMode is shared isolated none — see Core concepts. RenderMode is "ssr" | "ssg" | "csr" | "isr" | "streaming" — see Render modes.

Route modules

What a file under routes/ may export — nothing here is required:

ExportSignatureDoes
renderModeRenderModeHow this route renders — see Render modes
revalidate{ seconds: number }isr only — how long a render stays fresh
loader(ctx) => Promise<Data> | DataServer-side data for the page
getStaticParams() => Promise<Record<string,string>[]>Required for ssg/isr on a dynamic route — one static file per entry returned
action(formData, ctx) => Promise<unknown>Handles a form POST — returns redirect(to) (303), fail(status, data), or data for the page as actionData. File fields arrive as File objects
meta(data?) => { title?, description?, og?, head? }Page <title>, description, Open Graph, and route-level <head> tags
default export(props: RouteComponentProps) => JSXThe page's UI; props are { data?, csrfToken?, actionData? }

The request context (ctx)

Passed to every loader, action, serverFn, and API route handler. Sessions are opaque, server-side, and revocable, reached by an HttpOnly cookie or an Authorization: Bearer header through one lookup — see Security model. Checking who someone is stays your code's job.

MemberSignatureDoes
paramsRecord<string, string>Values from any [param] segments in the matched route. A [...slug] catch-all is in here too, its segments joined with / ("a/b")
catchAllRecord<string, string[]>The segments a [...slug] or [[...slug]] matched, as arrays (ctx.catchAll.slug); empty for a route with none (since 0.3.4)
urlURLThe request URL, query string included (ctx.url.searchParams.get("q")); scheme and host follow trustProxy
headersHeadersRequest headers (empty at build time for ssg)
methodstringThe HTTP method
ipstring | undefinedClient IP: the TCP peer, or from the right of X-Forwarded-For when trustProxy is set
requestRequestA standard Request without a body (the body goes to action)
sessionSessionData | undefinedThe current session's data, if any — from the cookie or a Bearer token alike. Typed once with declare module "@devorajs/core" { interface Register { session: { userId: string } } }; unknown otherwise
sessionTransport"cookie" | "bearer" | undefinedWhich transport authenticated this request
requireAuth()() => voidThrows a 401 HttpError if there's no valid session on either transport
setSession(data, options?)(data: unknown, { transport?: "cookie" | "bearer" }?) => Promise<string>Starts a new server-side session holding data and resolves to its ID. "cookie" (the default for a request that isn't already Bearer-authenticated) sets the session cookie; "bearer" sets none — return the ID to the client. Always issues a fresh ID, revoking any session the request already had
revokeSession(sessionId?)(sessionId?: string) => Promise<void>Deletes the current session server-side (and clears its cookie) — dead on both transports immediately. Pass another session's ID to revoke that one instead
clearSession()() => Promise<void>Alias for revokeSession() with no argument
verifyCsrf(submitted)(FormData | string) => voidThrows a 403 HttpError if the token doesn't match this browser's CSRF cookie — form posts pass FormData, a same-origin API call passes the header value as a plain string. Passes without a token for a Bearer-authenticated request

The framework waits for setSession/revokeSession's store write before sending the response even if you don't await it — but awaiting is clearer, and required if you need the ID. On an app with auth: "none", all five session methods above throw a clear error instead of silently no-opping if called — see Core concepts.

Actions, redirects & CSRF

import { fail, redirect, type RequestContext } from "@devorajs/core";

export async function action(formData: FormData, ctx: RequestContext) {
  ctx.verifyCsrf(formData);
  const username = String(formData.get("username") ?? "");
  if (!username) return fail(400, { error: "Enter a username" }); // re-render with 400, error in actionData
  // Check the password against your own database here, before this line.
  await ctx.setSession({ username }); // sets the HttpOnly session cookie
  return redirect("/dashboard"); // 303 See Other, so a refresh doesn't resubmit
}
ExportDoes
redirect(to, status?)303 after a POST, 302 from a loader; pass 301 | 302 | 303 | 307 | 308 to choose. A loader may also throw redirect(...)
fail(status, data)Re-render the page with a 4xx/5xx status and data as the actionData prop
CsrfField({ token })The hidden input a <form> needs; a page receives its own csrfToken as a prop
HttpError(status, message)Throw it from a loader, action or API route: the response keeps that status. On a page it renders the app's error.tsx
ErrorPageProps{ status, message }, the props of apps/<name>/error.tsx (also used for 404s). message is safe to show

Backend — serverFn, modules & API routes

serverFn(fn) — the original backend primitive: a plain function reference a route imports and calls directly, never dispatched dynamically by name. clientOnly(loader) guarantees a component never runs server-side (touches window at module scope, say).

import { serverFn } from "@devorajs/core";

export const updateSettings = serverFn(async (input, ctx) => {
  ctx.requireAuth();
  return db.settings.update(input);
});

defineModule(def, setup?) — organizes backend logic into scoped units, see Backend. A DevoraModule exposes .register(child) (mounts a child's routes, namespaced under its name) and .getRoutes() (the flattened table an app's dispatcher reads).

apiRoute(handler) — a plain request handler for a file in the app's API folder (apiDir: endpoints/ in new apps, api/ by default), served under /api, decoupled from page rendering. Optionally export a methods array alongside handler to reject unlisted HTTP methods with a real 405 before the handler runs.

API routes always answer in JSON since 0.3.0. apiRoute() wraps your handler (before 0.3.0 it returned it unchanged): an uncaught error becomes a { "message": "..." } JSON response instead of the dev server's HTML error page. Throw new HttpError(status, message) for a deliberate failure — its status and message are sent as-is. Any other error with a numeric status (or statusCode) from 400–599 is treated the same way, so an existing error class of your own keeps working. Anything else is a 500; its real message is shown in dev and replaced with "Internal Server Error" in production (the error is logged server-side either way). The dispatcher applies the same rule around every api/** request, so a handler exported without apiRoute(), or middleware wrapped outside it, doesn't produce HTML either.

// apps/web/endpoints/orders/[id].ts
import { apiRoute, HttpError } from "@devorajs/core";

const orders = new Map([
  ["1", { id: "1", total: 42 }],
  ["2", { id: "2", total: 5000 }],
]);

export const methods = ["GET"];
export const handler = apiRoute((req) => {
  const order = orders.get(req.params.id!);
  if (!order) throw new HttpError(404, "Order not found"); // → 404 {"message":"Order not found"}
  if (order.total > 1000) throw new Error("fraud check unavailable"); // → 500, message hidden in production
  return { status: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify(order) };
});

A request under /api that matches no route file — including bare /api — gets a JSON 404, { "message": "Not found" }, never an HTML page. Page routes are unaffected: their errors and 404s stay HTML.

TypeShape
ApiRequest{ method, url, headers, params, catchAll, body: Buffer } — params are strings, with a catch-all joined by /; catchAll has the arrays. body is raw bytes, not pre-parsed, so a webhook can verify an HMAC against the exact bytes first
ApiResponse{ status, headers?, body? }

Sessions — the store

defineSessionStore(store) — type helper for the default export of the module shared.sessions.store points at. A SessionStore is { get(key), set(key, record), delete(key) } (each may be sync or return a promise); a SessionRecord is { data, activeExpiresAt, expiresAt } (epoch milliseconds). The key is an HMAC of the session ID, never the ID itself. createMemorySessionStore() returns the in-process store "memory" uses — handy in tests. getSessionState(record) returns "active" | "idle" | "dead". See Security model for a working SQLite-backed store.

signSession/verifySession are still exported but deprecated — the framework no longer uses them, and a token signed with them can't be revoked. Replace a hand-rolled Bearer scheme built on them with setSession(data, { transport: "bearer" }).

Middleware

withMiddleware(handler, ...middlewares) — composes plain functions, left to right; the call site's argument order is the execution order. defineMiddleware(mw) exists only for type inference.

import { withMiddleware, apiRoute } from "@devorajs/core";

const logging = (next) => async (req, ctx) => {
  console.log(req.method, req.url);
  return next(req, ctx);
};

export const handler = withMiddleware(rawHandler, logging, requireAuthMw);

fromExpressMiddleware(mw) adapts an Express/Connect (req, res, next) middleware — supports reading req.method/url/headers and calling res.setHeader/getHeader/end/next(), not a full Node stream body or Express-specific extensions. fromFastifyPlugin(name, plugin) adapts a real Fastify plugin's route registrations into a DevoraModule — throws immediately if the plugin calls an unsupported method like addHook/decorate, rather than silently ignoring it.

Islands & client-only

import { island, Island } from "@devorajs/core";
// or "@devorajs/core/client" inside a csr route/island component itself

const ThreeScene = island(() => import("./ThreeScene"));

export default function Page() {
  return <Island component={ThreeScene} props={{ spin: true }} />;
}

island(() => import("specifier")) — only this literal call shape is accepted (no dynamic/computed specifiers). The build finds islands in any module the app imports, not only route files, and since 0.3.2 any other shape, or a path that doesn't resolve, fails devora dev/devora build with the file and line. <Island component props /> is what a route actually renders around one.

Dev reload safety

registerDisposable(fileUrlOrPath, dispose) — runs dispose right before Vite's dev server reloads the file at fileUrlOrPath (pass import.meta.url). Use it to close a stale native-addon DB connection cleanly instead of leaving it to a finalizer. Dev-only — nothing to dispose in a production process that never reloads modules. See Backend.

ISR revalidation

revalidatePath(staticOutDir, routePath) — forces an isr route's cached render to regenerate on its next request, ahead of its own revalidate window. See Render modes.