React Router v7

This guide starts with an existing React Router v7 framework-mode app. It is verified with v7.18.1; if your config does not recognize v8_viteEnvironmentApi, update to the latest v7 release.

Install Nitro and Workflow SDK

pnpm add nitro workflow

This integration requires Nitro v3.

Enable the Vite Environment API

React Router v7 keeps the Vite Environment API behind a future flag. Enable the required flag and set an explicit build directory:

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

export default {
  ssr: true,
  buildDirectory: "build",
  future: {
    v8_viteEnvironmentApi: true,
  },
} satisfies Config;

Create the React Router server handler

Create server/ssr.ts:

server/ssr.ts
import { createRequestHandler } from "react-router";

export default {
  fetch: createRequestHandler(
    () => import("virtual:react-router/server-build"),
    import.meta.env.MODE,
  ),
};

This adapts React Router's generated server build to the Fetch API handler Nitro expects.

Configure Vite

Update vite.config.ts:

vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { nitro } from "nitro/vite";
import { defineConfig } from "vite";
import { workflow } from "workflow/vite";
import reactRouterConfig from "./react-router.config";

export default defineConfig({
  plugins: [
    reactRouter(),
    nitro({
      serverDir: "./server",
      output: {
        dir: reactRouterConfig.buildDirectory,
        serverDir: `${reactRouterConfig.buildDirectory}/server`,
        publicDir: `${reactRouterConfig.buildDirectory}/client`,
      },
    }),
    workflow({ dirs: ["workflows"] }),
  ],
  environments: {
    ssr: {
      build: {
        rollupOptions: {
          input: "./server/ssr.ts",
        },
      },
    },
  },
});

Keep dirs: ["workflows"] so subsequent builds do not scan generated files under build. Place reactRouter() before nitro() in the plugin array.

Create a workflow

Create workflows/greeting.ts:

workflows/greeting.ts
export async function greetingWorkflow(name: string) {
  "use workflow";

  return greet(name);
}

async function greet(name: string) {
  "use step";

  return `Hello, ${name}!`;
}

Start the workflow from a Nitro route

Create server/routes/api/greeting.post.ts:

server/routes/api/greeting.post.ts
import { defineHandler } from "nitro";
import { start } from "workflow/api";
import { greetingWorkflow } from "../../../workflows/greeting";

export default defineHandler(async (event) => {
  const { name } = (await event.req.json()) as { name: string };
  const run = await start(greetingWorkflow, [name]);

  return { runId: run.runId };
});

React Router continues to handle your application routes. Nitro handles this server route at POST /api/greeting, as well as Workflow SDK's internal routes.

Run the app

Start the development server:

pnpm vite dev

Then start a workflow:

curl -X POST \
  -H "content-type: application/json" \
  -d '{"name":"Workflow"}' \
  http://localhost:3000/api/greeting

Build and start the production server:

pnpm vite build
node ./build/server/index.mjs

You can inspect local runs with pnpm workflow web.

Troubleshooting

Vite reports an invalid SSR input or path.replace is not a function

Set future.v8_viteEnvironmentApi to true in react-router.config.ts.

React Router pages return 404

Check that the ssr environment input points to ./server/ssr.ts.

A second build tries to compile files under build/server

Use workflow({ dirs: ["workflows"] }), remove the existing build directory once, and rebuild.

vite build finishes output but does not exit

Use workflow@5.0.0 or later with Nitro v3.