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

The fastest way to get started is with our project generator:

Navigate to your project directory:

Terminal
bash
$ cd my-rari-app

Start the development server:

Option 2: Add to Existing Vite Project

If you have an existing Vite + React project:

Update your vite.config.ts:

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):

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',
}

Create src/app/page.tsx (home page):

src/app/page.tsx
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):

src/components/Counter.tsx
'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:

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:

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',
}

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
└── .gitignore

Server 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
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 }
}

Use server actions in client components with 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>
  )
}

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