Server Setup
API routes, middleware and loaders are standard server modules. A fullstack app provides a server entry that composes them and hosts the request pipeline. The retired CLI no longer supplies that server automatically.
Native API server
This minimal entry mounts one API module on ES-Runtime:
import { serve } from "runtime:http"; import { env } from "runtime:process"; import { createApiHandler } from "@opentf/afw/server"; import * as hello from "./app/api/hello/route.js"; const api = createApiHandler({ "/app/api/hello/route.js": hello }); serve({ hostname: "127.0.0.1", port: Number(env.PORT || 8080) }, async (request) => await api(request) || new Response("Not found", { status: 404 }) );
Add a server target alongside the browser target in esdev.json:
"server": { "entry": "server.js", "out": "dist/server.js", "platform": "server" }
Use the default AFW plugin with mode: "ssr" when the server imports renderable components. Server modules compile to string renderers, while browser release modules compile for hydration.
esdev build --minify esdev dist/server.js
This example hosts APIs only. Static assets, SSR and additional routes belong in the app's server entry. The Server API reference describes the helpers for composing that pipeline.
Compose a full pipeline
Use createMiddleware around the request pipeline, then try API dispatch, loader data endpoints, static assets and page rendering. createLoaderRegistry supplies match, load and handle; renderRoute composes registered pages/layouts.
Thread the live request, query and middleware locals into loaders. Include serialized route data and island props in server HTML so client navigation and hydration resume with the same data. Each renderRoute() call isolates route values and island props across async work, so concurrent renders keep their own parameters and loader data.
Register pages for SSR
Import route registration from the server entry. It loads without a DOM or HTMLElement shim and shares the route table used by renderRoute():
import { registerRoutes, renderRoute } from "@opentf/afw/server"; import { pages } from "@afw/routes"; registerRoutes(pages); export async function renderPage(request, data) { const url = new URL(request.url); return renderRoute(url.pathname, null, url.search, { data }); }
The AFW plugin supplies @afw/routes and compiles server pages with mode: "ssr". The renderer returns HTML, metadata, hydration props and status; compose these into your HTML response. See Metadata & SEO for head rendering.
Server modules can also import router, configureI18n, matchRoute, resolveLocale, localizePath and i18nLocales from @opentf/afw/server. During rendering, router.params, router.query, router.locale and router.data refer to the current request. Browser components continue importing from @opentf/afw.
esdev create <dir> --template=fullstack generates this wiring: a server.js request pipeline, browser and server targets, and esdev start running the server. Server builds resolve page imports through the DOM-free @opentf/afw/server entry, so server.js is the server target directly; no HTMLElement stub is needed.
Other runtimes
For Fetch-native platforms, createFetchHandler composes dispatch and middleware with a fallback for assets or SSR. When your build script emits dist/server/api.js, see Fetch handler for adapter wiring.
Node applications import toNodeListener from @opentf/afw/server/adapters/node. Keep runtime-specific imports in the server entry, not in client components.
Environment and server-only imports
On ES-Runtime, read variables from runtime:process's env. Other hosts expose variables through their own environment API, and Fetch platforms can pass bindings as context.env/context.ctx. Configure environment loading for your chosen host; the retired CLI's automatic Bun .env loading is not part of esdev integration.
Keep database clients and secrets in loader.*, route.*, _middleware.* and server entries. The compiler does not provide a guarantee that arbitrary imports placed in client components stay server-only.
See API Routes, Middleware and Data Fetching.