1---
2title: React Router
3description: Deploy React Router v7 (framework mode) to Railway with Railway.Website.ReactRouter — SSR on a container Service, client assets in the image, and React Router's own Vite dev server under alchemy dev.
4sidebar:
5 order: 7
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12`Railway.Website.ReactRouter` deploys a
13[React Router](https://reactrouter.com) v7 app in framework mode to
14Railway. React Router builds through Vite, so Alchemy runs your
15project's own build: the SSR server plus client assets run on one
16[`Railway.Service`](/railway/compute/services). Alchemy creates a
17[`Railway.Project`](/railway/compute/projects) if you omit
18`project`. The URL is the generated `*.up.railway.app` hostname. No
19adapter.
20
21## Install
22
23The build integration is not bundled with alchemy. Install
24`@alchemy.run/frontend-frameworks`; the resource loads its
25`/react-router` and `/react-router/node` exports from your project at
26deploy time. It is only used at build time, so a dev dependency is
27enough:
28
29<Tabs syncKey="pkgManager">
30 <TabItem label="bun" icon="bun">
31 <Code code={`bun add -d ${devDeps}`} lang="sh" />
32 </TabItem>
33 <TabItem label="npm" icon="npm">
34 <Code code={`npm install -D ${devDeps}`} lang="sh" />
35 </TabItem>
36 <TabItem label="pnpm" icon="pnpm">
37 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
38 </TabItem>
39 <TabItem label="yarn" icon="seti:yarn">
40 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
41 </TabItem>
42</Tabs>
43
44The React Router side is whatever `npx create-react-router@latest`
45scaffolds — `react-router`, `@react-router/dev`, `@react-router/node`,
46`isbot`, and `vite`. Keep `@react-router/node` and `isbot` in
47`dependencies`: React Router picks its default `entry.server` by reading
48them from your `package.json`, and installs `isbot` on the fly if it is
49missing.
50
51## Configure Vite
52
53Your `vite.config.ts` stays what the React Router scaffold gives
54you — no adapter, no server preset. Vite plugins like Tailwind go in
55`plugins` as usual:
56
57```typescript
58// vite.config.ts
59import { reactRouter } from "@react-router/dev/vite";
60import tailwindcss from "@tailwindcss/vite";
61import { defineConfig } from "vite";
62
63export default defineConfig({
64 plugins: [tailwindcss(), reactRouter()],
65});
66```
67
68`reactRouter()` owns routing, typegen, and both build environments.
69Alchemy drives the same two passes `react-router build` runs, wraps the
70server manifest with `createRequestHandler`, and packages the resulting
71fetch handler as a Node HTTP server on port 3000.
72
73:::caution[Remove @cloudflare/vite-plugin if present]
74The React Router CLI adds `@cloudflare/vite-plugin` when you pick the
75Cloudflare template. It builds for workerd, not Node, so a Railway
76deploy needs it gone:
77
78```diff lang="typescript"
79// vite.config.ts
80-import { cloudflare } from "@cloudflare/vite-plugin";
81import { reactRouter } from "@react-router/dev/vite";
82import { defineConfig } from "vite";
83
84export default defineConfig({
85- plugins: [cloudflare({ viteEnvironment: { name: "ssr" } }), reactRouter()],
86+ plugins: [reactRouter()],
87});
88```
89
90:::
91
92## Declare the Website
93
94Declare the site as a module-level const (rather than inline in the
95Stack):
96
97```typescript
98// alchemy.run.ts
99import * as Railway from "alchemy/Railway";
100
101export const Website = Railway.Website.ReactRouter("Website");
102```
103
104Pass `project` to put the Service in an existing `Railway.Project`.
105`rootDir` is the project root — the directory holding `package.json`,
106`vite.config.*`, and `react-router.config.ts`. It defaults to `"."`,
107so set it only when the app does not sit next to `alchemy.run.ts`:
108
109```typescript
110export const Website = Railway.Website.ReactRouter("Website", {
111 rootDir: "./web",
112});
113```
114
115## Add it to the Stack
116
117Yield the site from your Stack and return its URL:
118
119```typescript
120// alchemy.run.ts
121import * as Alchemy from "alchemy";
122import * as Effect from "effect/Effect";
123
124export default Alchemy.Stack(
125 "MyReactRouterSite",
126 {
127 providers: Railway.providers(),
128 state: Alchemy.localState(),
129 },
130 Effect.gen(function* () {
131 const site = yield* Website;
132 return { url: site.url };
133 }),
134);
135```
136
137Requests matching a built client asset are served first; everything
138else — SSR routes, loaders, actions, resource routes — falls through to
139the Node handler on the Service.
140
141The live `url` is `https://{name}.up.railway.app`. `site.service`
142and `site.project` are the Railway resources underneath —
143`undefined` during `alchemy dev`.
144
145See the
146[React Router API reference](/providers/railway/website/reactrouter)
147for every prop and attribute.
148
149## How the two build outputs are deployed
150
151`react-router build` writes two directories, and both land in the
152Service image:
153
154```
155build/
156├── client/ -> baked into the image, served first
157└── server/ -> wrapped as a Node HTTP server on port 3000
158```
159
160`build/client` holds the hashed browser bundles plus everything you put
161in `public/`. `build/server/index.js` is a `ServerBuild` manifest, not a
162request handler, so Alchemy wraps it with `createRequestHandler` during
163the build and emits a Node serve entry — that process is the
164[`Railway.Service`](/railway/compute/services).
165
166If you set Vite's `base`, set React Router's `basename` to the same
167prefix: the client directory maps onto the site root one-for-one, so the
168paths the SSR document emits have to be the paths the image was given.
169
170## Add environment variables
171
172Process environment is a top-level `env` map — plain strings, or
173`Redacted` values:
174
175```diff lang="typescript"
176// alchemy.run.ts
177export const Website = Railway.Website.ReactRouter("Website", {
178+ env: {
179+ API_BASE: "https://api.example.com",
180+ },
181});
182```
183
184The values are copied onto `process.env` before the build and the
185dev server, and onto the Service at deploy time, so server code
186reads the same keys in both modes. Client inlining is whatever Vite
187does (`VITE_` prefixes).
188
189An output from another resource only exists once that resource is
190yielded, so declare the site inside the Stack generator instead:
191
192```typescript
193// alchemy.run.ts
194Effect.gen(function* () {
195 const api = yield* Api;
196
197 const site = yield* Railway.Website.ReactRouter("Website", {
198 env: { API_BASE: api.url },
199 });
200
201 return { url: site.url };
202});
203```
204
205## Read the environment in a loader
206
207The React Router server is a plain Node process. Loaders and
208actions read the environment from `process.env`:
209
210```typescript
211// app/routes/home.tsx
212export function loader() {
213 return { apiBase: process.env.API_BASE ?? "unset" };
214}
215```
216
217Values prefixed `VITE_` are inlined into the client bundle at build time
218instead, as `import.meta.env.VITE_*` — set those in the shell that runs
219`alchemy deploy`, not in `env`.
220
221## Read the environment in a resource route
222
223A route module with no default export is a resource route, served by the
224same Node process:
225
226```typescript
227// app/routes/api.hello.ts
228export function loader() {
229 return new Response(process.env.API_BASE ?? "unset");
230}
231```
232
233## Build configuration
234
235The build runs through your project's own `vite.config.ts` and
236`react-router.config.ts`. A custom `buildDirectory` just works — the
237integration observes the directory from the resolved Vite config, so
238there is nothing to mirror on the resource.
239
240## Local dev
241
242```sh
243bun alchemy dev
244```
245
246`alchemy dev` runs React Router's own Vite dev server (native HMR)
247instead of deploying; `site.url` is the local address and no Railway
248resources are created. Wrap the site in
249`Alchemy.remote()` to deploy the live Service even during dev:
250
251```typescript
252export const Website = Railway.Website.ReactRouter("Website").pipe(
253 Alchemy.remote(),
254);
255```
256
257## React Server Components
258
259RSC is not supported yet. React Router's `unstable` RSC plugin — and
260the `future.v8_viteEnvironmentApi` flag — emit more than one server
261environment, and this resource assembles a single Node entry. A build
262that produces no single server entry fails with an actionable error
263rather than deploying something broken.
264
265## Custom domain
266
267```typescript
268const site = yield* Railway.Website.ReactRouter("Web", {
269 domain: "app.example.com",
270});
271```
272
273`domain` attaches a [`Railway.CustomDomain`](/railway/networking)
274(`targetPort` 3000). `url` becomes `https://app.example.com`
275instead of the generated `*.up.railway.app`.
276
277## Where next
278
279- [React Router API reference](/providers/railway/website/reactrouter)
280- [Projects](/railway/compute/projects) and
281 [Services](/railway/compute/services)
282- [Custom domains](/railway/networking)