Skip to content

Quick start

A running app in one command, or added to a project you already have.

Updated View as Markdown

A new app

bun create rsc-kit@latest my-app
cd my-app
bun run dev

That is it. You get a page, a layout, a client component and a Vite config, wired together and running. There is no server file — Nitro builds one from the preset in that config when you build.

It asks a few questions, each with a flag for a script: where it runs, the React Compiler, Tailwind, oxlint, and which validation library you want — Zod, Valibot or ArkType. That one is asked once because everything schema-shaped hangs off it: a form’s schema, action.input(), a page’s searchParams, and typed environment variables, which it offers next. Say yes and you get src/env.ts:

src/env.tsts
export const env = createEnv({
  server: {
    NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
    // DATABASE_URL: z.url(),
  },
  clientPrefix: 'VITE_',
  client: {},
  // No bare process.env: import.meta.env carries the VITE_ values, and
  // the file does not throw in the browser if a client component imports it.
  runtimeEnv: { ...(typeof process === 'undefined' ? {} : process.env), ...import.meta.env },
  emptyStringAsUndefined: true,
})

A missing or malformed variable is refused at startup with its name, not as an undefined three calls later; env.DATABASE_URL is typed everywhere it is read; and a server variable can never reach the browser — a browser-readable one has to start with VITE_. That is @t3-oss/env-core, with the schema in whichever library you chose. .env.example is written beside it and is the one that gets committed.

Import env from server code. A client component reads its VITE_ value from import.meta.env.VITE_… directly: importing src/env.ts there brings the validator along to read one string, about 20 kB gzipped for Zod. The value is still checked, because the server imports env when it starts.

bun create rsc-kit@latest my-app --validation=valibot --env   # scripted

When you are ready to ship:

bun run build
bun run start

The build prints what it did:

  ○  /

  ○  (Static)  prerendered as static content

  1 static

Pages marked ○ are static: rendered once at build time rather than for each visitor.

An app you already have

Already have a project? Add rsc-kit to it:

bunx rsc-kit@latest init

It reads your package.json, works out which server you use and where your source lives, and writes only what is missing. A project with no package.json — a Go or Rust service getting its frontend — is given its first one. And in an empty directory, or a fresh clone of an empty repository, there is nothing to add to, so init starts a new app right there, leaving the README alone and adding what the app needs ignored to the .gitignore:

  server      hono
  source      src
  react       will be added

  + src/app/layout.tsx
  + src/app/page.tsx
  + vite.config.ts
  ~ package.json  — added @rsc-kit/core, react, react-dom, vite, nitro, …
  ~ scripts  — dev, build, start, compile, typecheck

No server file. Nitro builds one around the route tree, so the app owns a route tree and a vite config and nothing in between.

It never overwrites anything. If you already have a vite.config.ts, it prints the edit to make instead of replacing work you have done. Running it twice is safe — the second run just tells you what is already in place.

Then:

bun install
bun run dev

Add a page

Routes are directories under src/app. Create a folder, put a page.tsx in it, and it exists:

src/app/about/page.tsxtsx
export default function AboutPage() {
  return <h1>About</h1>
}
bun run build   # the route tree is read at build time

Doing it by hand

If you would rather wire it up yourself — or the CLI cannot reach your setup — Installation walks through the same result one file at a time.

Where next

Navigation

Type to search…

↑↓ navigate↵ selectEsc close