Skip to main content

react-router.config.ts

Configures React Router application settings. Located at react-router.config.ts in your project root.

Import

import type { Config } from "@react-router/dev/config";

ReactRouterConfig Type

export interface ReactRouterConfig {
  appDirectory?: string;
  basename?: string;
  buildDirectory?: string;
  buildEnd?: BuildEndHook;
  future?: Partial<FutureConfig>;
  prerender?: PrerenderConfig;
  presets?: Preset[];
  routeDiscovery?: RouteDiscoveryConfig;
  serverBuildFile?: string;
  serverBundles?: ServerBundlesFunction;
  serverModuleFormat?: "esm" | "cjs";
  ssr?: boolean;
  allowedActionOrigins?: string[];
}

Configuration Options

appDirectory

appDirectory
string
default:"app"
Path to the application directory, relative to project root.
export default {
  appDirectory: "src",
};

basename

basename
string
default:"/"
Base path for all routes. Useful when your app is served from a subdirectory.
export default {
  basename: "/my-app",
};
Must match Vite’s base config:
export default defineConfig({
  base: "/my-app",
  plugins: [reactRouter()],
});

buildDirectory

buildDirectory
string
default:"build"
Path to the build output directory, relative to project root.
export default {
  buildDirectory: "dist",
};
Output structure:
dist/
├── client/      # Client assets
└── server/      # Server bundle(s)

buildEnd

buildEnd
function
Hook called after build completes. Useful for custom post-build tasks.
type BuildEndHook = (args: {
  buildManifest: BuildManifest | undefined;
  reactRouterConfig: ResolvedReactRouterConfig;
  viteConfig: ResolvedConfig;
}) => void | Promise<void>;
Example:
export default {
  async buildEnd({ buildManifest, viteConfig }) {
    // Generate custom manifest
    await writeFile(
      "build/custom-manifest.json",
      JSON.stringify(buildManifest)
    );
    
    // Copy additional files
    await cp("public", "build/client/public", { recursive: true });
  },
};

future

future
object
Enables future features and breaking changes.
interface FutureConfig {
  unstable_optimizeDeps: boolean;
  unstable_subResourceIntegrity: boolean;
  unstable_trailingSlashAwareDataRequests: boolean;
  unstable_previewServerPrerendering: boolean;
  v8_middleware: boolean;
  v8_splitRouteModules: boolean | "enforce";
  v8_viteEnvironmentApi: boolean;
}
Example:
export default {
  future: {
    v8_middleware: true,
    v8_splitRouteModules: "enforce",
  },
};
Flags:
  • unstable_optimizeDeps - Optimize dependency pre-bundling
  • unstable_subResourceIntegrity - Generate SRI hashes for scripts
  • unstable_trailingSlashAwareDataRequests - Handle trailing slashes in data requests
  • unstable_previewServerPrerendering - Prerender with Vite preview server
  • v8_middleware - Enable route middleware
  • v8_splitRouteModules - Automatic route code splitting (true | "enforce")
  • v8_viteEnvironmentApi - Use Vite Environment API

prerender

prerender
boolean | string[] | function | object
Defines which routes to prerender at build time as static HTML.Types:
type PrerenderConfig = 
  | boolean  // true = all routes, false = none
  | string[] // Specific paths
  | ((args: { getStaticPaths: () => string[] }) => string[] | Promise<string[]>)
  | {
      paths: boolean | string[] | Function;
      unstable_concurrency?: number;
    };
Examples:
export default {
  prerender: true,
};

presets

presets
Preset[]
Configuration presets for platform integrations.
interface Preset {
  name: string;
  reactRouterConfig?: (args: {
    reactRouterUserConfig: ReactRouterConfig;
  }) => ConfigPreset | Promise<ConfigPreset>;
  reactRouterConfigResolved?: (args: {
    reactRouterConfig: ResolvedReactRouterConfig;
  }) => void | Promise<void>;
}
Example:
import { cloudflarePreset } from "@react-router/cloudflare";

export default {
  presets: [cloudflarePreset()],
};

routeDiscovery

routeDiscovery
object
Controls lazy route discovery behavior.
type RouteDiscoveryConfig =
  | { mode: "lazy"; manifestPath?: string }
  | { mode: "initial" };
Default: { mode: "lazy", manifestPath: "/__manifest" } (when SSR enabled)Examples:
export default {
  routeDiscovery: {
    mode: "lazy",
    manifestPath: "/__manifest", // Custom path
  },
};
Lazy mode requires SSR. With ssr: false, mode is automatically "initial".

serverBuildFile

serverBuildFile
string
default:"index.js"
Filename for the server build output.
export default {
  serverBuildFile: "server.js",
};
Output: build/server/server.js

serverBundles

serverBundles
function
Split server code into multiple bundles based on route.
type ServerBundlesFunction = (args: {
  branch: BranchRoute[];
}) => string | Promise<string>;

interface BranchRoute {
  id: string;
  path?: string;
  file: string;
  index?: boolean;
}
Example:
export default {
  serverBundles({ branch }) {
    // Check if any route in branch starts with "routes/admin"
    const isAdminRoute = branch.some(
      (route) => route.id.startsWith("routes/admin")
    );
    
    const isApiRoute = branch.some(
      (route) => route.id.startsWith("routes/api")
    );
    
    if (isAdminRoute) return "admin";
    if (isApiRoute) return "api";
    return "main";
  },
};
Outputs:
build/server/
├── main/
│   └── index.js
├── admin/
│   └── index.js
└── api/
    └── index.js

serverModuleFormat

serverModuleFormat
'esm' | 'cjs'
default:"esm"
Module format for server build output.
export default {
  serverModuleFormat: "cjs",
};
Most modern runtimes support ESM. Use CJS only if required by your deployment platform.

ssr

ssr
boolean
default:"true"
Enables server-side rendering. Set to false for SPA mode.
export default {
  ssr: false, // SPA mode
};
SPA Mode:
  • Pre-renders / at build time
  • Saves as index.html
  • No server required
  • All routes client-side only

allowedActionOrigins

allowedActionOrigins
string[]
Whitelist of allowed origins for form submissions. Supports glob patterns.
export default {
  allowedActionOrigins: [
    "example.com",
    "*.example.com",      // sub.example.com
    "**.example.com",     // sub.domain.example.com
  ],
};
Does not apply to resource routes (routes without UI components).
Runtime Override:
import type { ServerBuild } from "react-router";

async function getBuild(): Promise<ServerBuild> {
  const build = await import("./build/server/index.js");
  return {
    ...build,
    allowedActionOrigins: [
      process.env.ALLOWED_ORIGIN,
    ],
  };
}

Complete Example

import type { Config } from "@react-router/dev/config";

export default {
  appDirectory: "app",
  basename: "/",
  buildDirectory: "build",
  
  future: {
    v8_middleware: true,
    v8_splitRouteModules: "enforce",
  },
  
  async prerender({ getStaticPaths }) {
    const posts = await fetchPosts();
    return [
      "/",
      "/about",
      ...posts.map(p => `/blog/${p.slug}`),
      ...getStaticPaths(),
    ];
  },
  
  routeDiscovery: {
    mode: "lazy",
  },
  
  serverBundles({ branch }) {
    const isAdminRoute = branch.some(
      route => route.id.startsWith("routes/admin")
    );
    return isAdminRoute ? "admin" : "main";
  },
  
  serverModuleFormat: "esm",
  ssr: true,
  
  allowedActionOrigins: [
    "example.com",
    "*.example.com",
  ],
  
  async buildEnd({ buildManifest }) {
    console.log("Build completed!");
    // Custom post-build logic
  },
} satisfies Config;

Common Patterns

Multi-Region Deployment

export default {
  serverBundles({ branch }) {
    // Split by geographic region based on route prefix
    const path = branch[branch.length - 1]?.path || "";
    
    if (path.startsWith("/eu")) return "eu";
    if (path.startsWith("/asia")) return "asia";
    return "us";
  },
};

Development vs Production

const isDev = process.env.NODE_ENV === "development";

export default {
  buildDirectory: isDev ? "dev-build" : "build",
  
  future: {
    v8_middleware: !isDev, // Only in production
  },
  
  prerender: isDev ? false : ["/", "/about"],
};

Incremental Static Regeneration Pattern

export default {
  async prerender({ getStaticPaths }) {
    // Only prerender popular pages at build time
    const popular = await getPopularPages();
    return [
      "/",
      ...popular,
      // Other pages rendered on-demand
    ];
  },
};

Custom Build Artifacts

import { writeFile } from "fs/promises";

export default {
  async buildEnd({ buildManifest, reactRouterConfig }) {
    // Generate custom route manifest for analytics
    const routes = Object.keys(buildManifest.routes);
    await writeFile(
      "build/routes.json",
      JSON.stringify(routes, null, 2)
    );
    
    // Copy additional assets
    await cp(
      "locales",
      `${reactRouterConfig.buildDirectory}/client/locales`,
      { recursive: true }
    );
  },
};

TypeScript

Ensure proper type checking:
import type { Config } from "@react-router/dev/config";

export default {
  appDirectory: "app",
  ssr: true,
  // ...
} satisfies Config;

Validation

Config is validated at build time:
// ✅ Valid
export default {
  appDirectory: "app",
  ssr: true,
};

// ❌ Invalid - wrong type
export default {
  ssr: "yes", // Error: Expected boolean
};

// ❌ Invalid - incompatible options
export default {
  ssr: false,
  routeDiscovery: { mode: "lazy" }, // Error: lazy requires SSR
};

Build docs developers (and LLMs) love