Backend-Only Apps
Run Farm as an API server with no pages, no browser bundle, and no React: set renderer to false.
A backend-only app keeps Farm's server features: file and programmatic API routes, validated endpoints, middleware, the generated API client, OpenAPI, health checks, graceful shutdown, storage, cache, cron, observability, and every deployment target. It drops everything that only serves pages.
Set up
import { defineConfig } from "@farm.js/core";
export default defineConfig({
renderer: false,
// Optional: serve src/app/api/** from the root, so /todos answers as well as /api/todos.
api: { basePath: "/" },
});Install Farm without React:
pnpm add @farm.js/core
pnpm add -D @farm.js/cli vite typescript{
"type": "module",
"scripts": {
"dev": "farm dev",
"build": "farm build --preset node-server",
"start": "farm start"
}
}farm dev reloads routes as you edit them. farm build writes the server to .farm/.output, and
farm start runs it. The server listens on HOST and PORT, so there is no listen() call to
write.
Routes
Every route.ts under src/app/api is an endpoint, and its folder path is the URL:
src/app/api/route.ts -> /api (and / with api.basePath "/")
src/app/api/todos/route.ts -> /api/todos
src/app/api/todos/[id]/route.ts -> /api/todos/:idimport { createEndpoint } from "@farm.js/core/api";
import { z } from "zod";
import { createTodo, listTodos } from "../../../lib/todos";
export function GET() {
return Response.json({ todos: listTodos() });
}
export const POST = createEndpoint(
{ body: z.object({ title: z.string().min(1) }) },
async ({ body }) => Response.json(createTodo(body.title), { status: 201 }),
);See API Routes for validation, typed errors, uploads, and streaming, and API Client for the typed client Farm generates from these files.
To list routes in one file instead, export defineRoutes from src/routes.ts. Its paths need no
/api prefix:
import { defineRoutes } from "@farm.js/core/routes";
export default defineRoutes(({ api }) => [
api("/", { GET: () => Response.json({ status: "ok" }) }),
api("/users/[id]", {
GET: (request) => Response.json({ path: new URL(request.url).pathname }),
}),
]);Both styles can live in one app. Routes from src/routes.ts are not in the generated API client
yet; see Programmatic routes.
Middleware
src/app/**/middleware.ts files run before the matching routes, as in any Farm app. Answer early by
returning a Response:
import type { RequestMiddlewareContext } from "@farm.js/core/middleware";
export function middleware(request: Request, context: RequestMiddlewareContext) {
context.headers.set("x-request-id", crypto.randomUUID());
if (request.method !== "GET" && !request.headers.get("authorization")) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
}Import from server entries such as @farm.js/core/api, @farm.js/core/routes, and
@farm.js/core/middleware. The package root and @farm.js/core/navigation (redirect(),
notFound()) are page APIs that load React; return a Response instead.
What changes
With renderer: false, Farm:
- builds no browser client and no server-side renderer, and does not load React in development or production;
- answers a request that matches no route with JSON:
404 {"error":"Not found","pathname":"/..."}, including a miss underapi.basePath: "/"; - defaults
imagestoprovider: "none", so the image optimizer and Sharp stay out of the server; - turns off the page-derived agent defaults: JSON-LD,
/llms.txt,/sitemap.xml, and the read-only/mcpserver that reads pages. Set any of them underagentto turn it back on; - fails the build, and
farm dev, if the app has a page or layout, naming each file; - skips the page and root-layout checks in
farm doctorand DevTools, and checks for API routes instead.
Built-in health probes (/_farm/health/live, /_farm/health/ready), graceful shutdown, and every
deployment target work as in any other app.
Plugins
Server plugins work as in any app: Health, API MCP,
Sentry, Content, Sync,
Plugin Tables, Analyzer (it reports the
server bundle), WebAssembly for .wasm imports in API routes,
MSW for server-side mocking, and plugins you write with
definePlugin: setup, runtime hooks, routes, and lifecycle
hooks all run.
Plugins that only serve pages stop farm dev and farm build with a message naming
renderer: false: Partytown, PWA,
Search, StyleX, WebMCP,
Federation, and MSW with browser: true.
Development plugins that act on pages have no effect: Hints,
Progress, and Scripts.
DevTools opens its standalone panel at /__farm/devtools, which lists the
app's API routes, middleware, and plugins.
A plugin must import from Farm's subpath entries, such as @farm.js/core/plugin; the package root
loads React. The Blyp integration imports the root today, so it needs React
installed.
Integrations
Server integrations load without React: AI, Agent, Auth0, Auth.js, Autumn, Better Auth, Clerk,
Convex, Jobs, Polar, Sanity, Strapi, Stripe, Supabase, Unkey, and WorkOS.
Email renders React Email templates, so it needs React installed. An
integration you write imports defineIntegration and integrationRoute from
@farm.js/core/integrations, and its client bindings import api from @farm.js/core/api/client.
Testing
createFarmTestHarness from @farm.js/core/testing calls API routes without React. Only its
page() helper needs a renderer. See Testing.
Example
examples/api-server is a
todo API with validated endpoints, a middleware that requires a token for writes, JSON 404s, and an
OpenAPI document at /openapi.json.