Description:
rari is a React Server Components framework that runs on a Rust-powered runtime instead of Node.js.
It consists of three layers: a Rust HTTP server with an embedded V8 engine handling routing and RSC rendering, a React framework layer managing the app router, server actions, and streaming via Suspense, and a build toolchain built on Rolldown-Vite for bundling with tsgo for TypeScript type checking.
The architecture produces measurable throughput differences compared to Node-based React frameworks. Under 50 concurrent connections over 30 seconds, rari processes 74,662 requests per second against Next.js at 1,605, a 46.5x gap. Single-request average response time sits at 0.43ms versus 3.92ms. Client bundle output comes in at 264 KB compared to 562 KB.
How to Use It
Prerequisites
You need Node.js 22.0 or higher and a modern package manager. The rari team recommends pnpm throughout their documentation.
Option 1: Create a New Project
Run the project generator to scaffold everything from scratch:
pnpm create rari-app@latest my-rari-appMove into the new directory:
cd my-rari-appStart the development server:
pnpm devThe app becomes available at http://localhost:5173. The dev server runs hot module reloading, an error overlay, and TypeScript support out of the box.
Option 2: Add to an Existing Vite Project
If you already have a Vite + React project, install the rari package:
pnpm add rariThen update your vite.config.ts to register the plugin:
// vite.config.ts
import { rari } from 'rari/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
rari(),
],
})Building Your First App Router Page
All app router files live in src/app/. Start with the root layout.
Root Layout — src/app/layout.tsx
import type { LayoutProps } from 'rari'
export default function RootLayout({ children }: LayoutProps) {
return (
<div>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<main>{children}</main>
</div>
)
}
export const metadata = {
title: 'My rari App',
description: 'Built with rari',
}Home Page — src/app/page.tsx
This file is a React Server Component by default. It runs on the server, fetches data via await, and passes the result directly into the JSX tree. No client-side fetch needed.
import type { PageProps } from 'rari'
import Counter from '@/components/Counter'
export default async function HomePage({ params, searchParams }: PageProps) {
const response = await fetch('https://api.github.com/repos/facebook/react')
const repoData = await response.json()
return (
<div>
<h1>Welcome to rari</h1>
<div>
<h2>React Repository Stats</h2>
<p>Stars: {repoData.stargazers_count.toLocaleString()}</p>
<p>Forks: {repoData.forks_count.toLocaleString()}</p>
<p>Watchers: {repoData.watchers_count.toLocaleString()}</p>
<p>Last updated: {new Date(repoData.updated_at).toLocaleDateString()}</p>
</div>
<Counter />
</div>
)
}
export const metadata = {
title: 'Home | My rari App',
description: 'Welcome to my rari application',
}Client Component — src/components/Counter.tsx
Any component that uses hooks, event handlers, or browser APIs requires the 'use client' directive at the top.
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<h2>Client Interaction</h2>
<button onClick={() => setCount(count + 1)} type="button">
Count: {count}
</button>
</div>
)
}Server vs. Client Components
Server Components run during rendering on the Rust backend. They support async/await, server-only APIs, and direct database or API calls. They cannot access browser APIs or attach event handlers.
Client Components run in the browser. They support React hooks, event handlers, and all browser APIs. They cannot call server-only code directly. Mark them with 'use client' as the first line of the file.
// Server component — no directive needed
export default async function ServerComponent() {
const data = await fetch('https://api.example.com/data')
const result = await data.json()
return <div>{result.message}</div>
}// Client component
'use client'
import { useState } from 'react'
export default function ClientComponent() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)} type="button">
Clicked {count} times
</button>
)
}Server Actions
Server actions are async functions marked with 'use server'. They run on the server and are callable from client components. Place them in src/actions/ for organizational clarity.
src/actions/user-actions.ts
'use server'
export async function createUser(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
const user = await database.users.create({ name, email })
return { success: true, user }
}
export async function deleteUser(id: string) {
await database.users.delete(id)
return { success: true }
}Call the server action from a client component using useActionState:
src/components/UserForm.tsx
'use client'
import { useActionState } from 'react'
import { createUser } from '@/actions/user-actions'
export default function UserForm() {
const [state, formAction, isPending] = useActionState(createUser, null)
return (
<form action={formAction}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create User'}
</button>
{state?.success && <p>User created successfully!</p>}
</form>
)
}Using npm Packages
rari’s Rust runtime resolves node_modules the same way Node.js does. Standard package.json imports work without URL specifiers. Install a package normally and import from it directly.
pnpm add markedsrc/components/MarkdownPost.tsx
import { marked } from 'marked'
interface MarkdownPostProps {
content: string
title: string
}
export default async function MarkdownPost({ content, title }: MarkdownPostProps) {
marked.setOptions({ gfm: true, breaks: false })
const htmlContent = await marked.parse(content)
return (
<article>
<h1>{title}</h1>
<div dangerouslySetInnerHTML={{ __html: htmlContent }} />
</article>
)
}Use the component inside a page:
src/app/blog/page.tsx
import MarkdownPost from '@/components/MarkdownPost'
const blogPost = `
# Welcome to rari!
This markdown is processed on the server using the \`marked\` package.
- Fast server-side rendering
- Universal NPM package support
- Zero configuration required
`
export default function BlogPage() {
return (
<div>
<MarkdownPost title="My Blog Post" content={blogPost} />
</div>
)
}
export const metadata = {
title: 'Blog | My rari App',
description: 'Read our latest posts',
}Production Build and Deployment
Build the production bundle:
pnpm buildThe build output includes automatic code splitting, asset optimization, and a server bundle. Then start the production server:
pnpm startRelated Resources
- React Server Components Docs: The official React documentation covering RSC architecture, serialization rules, and the server/client boundary.
- Vite: The build tool rari’s existing-project integration targets. Understanding Vite’s plugin API helps when customizing the rari Vite setup.
- Rolldown: The Rust-based bundler underlying rari’s build toolchain. Relevant for developers who want to understand the build performance gains.
- tsgo: Microsoft’s Go-based TypeScript type checker that rari uses for fast type validation during builds.
FAQs
Q: Can rari run existing Next.js projects?
A: No. rari is not a drop-in Next.js replacement. It uses a compatible file-based router and RSC model, but the runtime, build system, and deployment target are different. Migration requires rewriting configuration files and removing Next.js-specific APIs.
Q: Does rari support dynamic route segments?
A: Yes. Wrap a folder name in square brackets inside src/app/ to create a dynamic segment. For example, src/app/users/[id]/page.tsx exposes the id parameter through the params prop on the page component.
Q: How do Suspense boundaries work with streaming SSR?
A: Wrap any async server component subtree in a React <Suspense> boundary with a fallback prop. rari flushes the fallback HTML immediately and streams the resolved content to the browser as data becomes available.
Q: Does rari require Rust installed on the developer machine?
A: No. Pre-built Rust binaries ship with the npm package. You do not need the Rust toolchain or cargo on your machine to run or build a rari project.
Q: Is server action state preserved across re-renders?
A: useActionState from React manages the state returned by a server action across submissions. The hook tracks pending status and the last returned value. State resets when the component unmounts unless you manage it explicitly in a parent.