Farm.js

Layers

Farm layers let an application inherit routes, layouts, middleware, plugins, integrations, components, configuration defaults, and generated types from ordinary Farm-shaped directories or installed packages.

A layer does not call a special registration function. The consuming application's extends array is what makes a directory or package a layer.

Consume layers

import { defineConfig } from "@farm.js/core";

export default defineConfig({
  extends: ["@company/farm-base", "@company/admin-layer", "./layers/commerce"],
});

Farm applies entries from left to right. In this example, admin-layer can override farm-base, commerce can override both, and the current project has final priority.

Declare a layer

A local layer uses the same shape as a small Farm application:

layers/commerce/
  farm.config.ts
  src/
    app/
      products/
        page.tsx
      cart/
        page.tsx
      checkout/
        layout.tsx
        page.tsx
    components/
      ProductPrice.tsx
    farm.routes.tsx

farm.config.ts is optional. When present, it exports a plain object:

import type { FarmLayerConfig } from "@farm.js/core";
import { commercePlugin } from "./src/plugin";

export default {
  plugins: [commercePlugin()],

  routeRules: {
    "/products/**": { swr: 300 },
    "/checkout/**": { render: "dynamic" },
  },
} satisfies FarmLayerConfig;

FarmLayerConfig is an optional type-only helper. A plain default object works without importing anything, and defineConfig works too. There is no defineFarmLayer API.

Override layer files

Suppose commerce provides:

layers/commerce/src/app/products/page.tsx

The project replaces only that page by creating:

src/app/products/page.tsx

The layer's product layout, loading boundary, middleware, API routes, and unrelated pages remain active. The same rule applies between layers: a later source replaces an earlier file with the same virtual route and file kind.

Duplicate routes inside one source are errors. A duplicate across sources is an intentional override.

Import layer components

Each layer receives an alias derived from its directory or package name:

import ProductPrice from "#layers/commerce/components/ProductPrice";
import AdminNavigation from "#layers/admin-layer/components/AdminNavigation";

Layer-owned files can continue using ordinary relative imports. Published packages may additionally expose components through their normal package exports.

Publish a package layer

An installed package needs no Farm-specific manifest:

@company/admin-layer/
  package.json
  farm.config.js
  src/
    app/
      admin/
        layout.tsx
        page.tsx
        users/page.tsx
    components/
      AdminNavigation.tsx

Ensure the published package includes farm.config.* and its source directory. Farm resolves the package root, loads the optional config, and scans its source just like a local layer.

Configuration composition

ConfigurationBehavior
Scalar valuesThe later layer or project value wins.
Plain objectsDeeply merged; later keys win.
pluginsAppended from the lowest layer through the project.
middleware configAppended in layer order.
redirects, rewrites, headersResults are appended in layer order.
Other arraysReplaced by the later value.
IntegrationsMerged by integration name; later definitions win.
Environment schemasMerged by server/public key; project definitions win conflicts.

The resolved package or directory root and its layer-local srcDir locate that layer's files. Build ownership stays with the project, so layers cannot replace project root, outDir, distDir, deployment target, output mode, public directory, preset, or build ID generator.

Security-sensitive arrays such as serverActions.allowedOrigins are replaced rather than concatenated. A project can therefore replace an inherited allowlist without accidentally appending to it.

Generated types

Farm generates route, API, environment, and static image types from the final resolved application graph. Layer routes appear in typed Link values, layer APIs appear in the generated API client, layer environment schemas participate in getEnv autocomplete, and raster imports carry image dimensions.

API route methods compose across layers. If a base layer exports GET and the project exports only POST at the same path, both methods remain available and generated types import each method from its real source. When a later layer or the project exports the same method, that method's generated type points to the higher-priority implementation.

Nested layers

A layer can extend lower-level layers using the same property:

// layers/commerce/farm.config.ts
export default {
  extends: ["@company/domain-base"],
};

Nested paths resolve relative to the layer that declares them. Farm deduplicates repeated layers and reports a readable error when it finds a cycle.

Development behavior

Adding or removing layer pages, layouts, boundaries, APIs, middleware, or programmatic route files refreshes route discovery and generated types. Editing an imported module uses normal Vite HMR.

Restart the development server after changing extends, a layer's srcDir, or layer configuration that changes the resolved plugin/config graph.

Best practices

  • Keep base layers broad and feature layers focused on one domain.
  • Put application-specific behavior in the project, where override intent is visible.
  • Prefer package versions or workspace dependencies over unpinned remote source.
  • Treat a layer as trusted executable code because its config, plugins, middleware, and server routes run inside the application.
  • Keep authentication and authorization inside inherited APIs and server functions; consuming a layer does not create a security boundary.