Getting Started with rari
This guide walks you through creating your first rari application. rari has three layers: a Rust runtime (HTTP server, RSC renderer, and router with embedded V8), a React framework (app router, server actions, streaming/Suspense), and a build toolchain (Rolldown-powered Vite bundling, tsgo type checking). You write standard React, the runtime underneath is Rust instead of Node.
Prerequisites
Before you begin, ensure you have the following installed:
- Node.js
22.12.0+(Active LTS release recommended) - A modern package manager (we recommend pnpm)
- A modern code editor
Installation
Option 1: Create New Project (Recommended)
The fastest way to get started is with our project generator:
Navigate to your project directory:
$ cd my-rari-appStart the development server:
Option 2: Add to Existing Vite Project
If you have an existing Vite + React project:
Update your vite.config.ts:
import { rari } from 'rari/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
rari(),
],
})Your First App Router Page
With the app router, your application structure is based on the src/app/ directory. Let's create your first page. See Routing for the full reference on file conventions, dynamic routes, and layouts.
Create src/app/layout.tsx (root layout):
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',
}Create src/app/page.tsx (home page):
import type { PageProps } from 'rari'
import Counter from '@/components/Counter'
// This is a React Server Component - runs on the server!
export default async function HomePage({ params, searchParams }: PageProps) {
// Fetch data on the server
const response = await fetch('https://api.github.com/repos/facebook/react')
const repoData = await response.json()
return (
<div>
<h1>Welcome to rari</h1>
{/* Server-rendered content */}
<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>
{/* Client Component */}
<Counter />
</div>
)
}
export const metadata = {
title: 'Home | My rari App',
description: 'Welcome to my rari application',
}Create src/components/Counter.tsx (client component):
'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>
)
}Using NPM Packages
The Rust runtime handles standard node_modules resolution. You can import from any npm package the same way you would in Node. Unlike most Rust-based JS runtimes, there are no URL imports or special npm specifiers. Your existing package.json workflow just works.
Create src/components/MarkdownPost.tsx:
import { marked } from 'marked'
interface MarkdownPostProps {
content: string
title: string
}
export default async function MarkdownPost({ content, title }: MarkdownPostProps) {
// Process markdown on the server
marked.setOptions({
gfm: true,
breaks: false,
})
const htmlContent = await marked.parse(content)
return (
<article>
<h1>{title}</h1>
<div dangerouslySetInnerHTML={{ __html: htmlContent }} />
</article>
)
}Use it in a page:
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',
}Development Workflow
Start Development Server
Your app will be available at http://localhost:5173 (Vite default) with:
- Hot module reloading for instant updates
- Error overlay for debugging
- TypeScript support out of the box
Build for Production
This creates an optimized production build with:
- Automatic code splitting
- Asset optimization
- Server bundle generation
Start Production Server
Runs your optimized app in production mode.
Project Structure
A typical rari project (created with create-rari-app) looks like this:
my-rari-app/
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── globals.css
│ │ ├── about/
│ │ │ └── page.tsx
│ │ ├── blog/
│ │ │ ├── page.tsx
│ │ │ └── [slug]/
│ │ │ └── page.tsx
│ │ └── users/
│ │ └── [id]/
│ │ └── page.tsx
│ ├── components/
│ │ └── Counter.tsx
│ └── actions/
│ └── todo-actions.ts
├── public/
├── index.html
├── package.json
├── vite.config.ts
├── tsconfig.json
└── .gitignoreServer vs Client Components
Server Components (Default)
- Run on the server during rendering
- Can use async/await and server-only APIs
- Cannot use browser APIs or event handlers
- Automatically serialized for the client
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 Components
- Run in the browser
- Can use hooks, event handlers, browser APIs
- Cannot use server-only APIs
- Use
'use client'directive to mark explicitly
'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
- Use
'use server'directive for server functions that can be called from client components - Place in
src/actions/directory for organization
'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 }
}Use server actions in client components with useActionState:
'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>
)
}Next Steps
- Routing: File conventions, dynamic routes, layouts, loading states, error boundaries, and API routes
- Deploying: Build and deploy to Railway, Render, or any Node.js host
- Metadata: Page titles, descriptions, Open Graph, and Twitter cards
- Image Component: Automatic image optimization
- ImageResponse: Dynamic Open Graph image generation