Frontend Setup
This guide walks you through setting up a React frontend with Vite for your Inertia.js + FastAPI application.
Prerequisites
Section titled “Prerequisites”- Node.js 18+ (or Bun)
- A FastAPI application with cross-inertia installed
Project Structure
Section titled “Project Structure”A typical Inertia.js + FastAPI project looks like this:
my-app/├── frontend/ # React frontend code│ ├── app.tsx # Inertia app entry point│ ├── globals.css # Global styles│ ├── components/ # Shared components│ │ └── Layout.tsx│ └── pages/ # Page components│ ├── Home.tsx│ └── Users/│ ├── Index.tsx│ └── Show.tsx├── templates/│ └── app.html # Root HTML template├── static/ # Static assets (built files go here)├── main.py # FastAPI application├── package.json├── vite.config.ts└── tsconfig.jsonStep 1: Initialize Frontend
Section titled “Step 1: Initialize Frontend”Create a package.json with the required dependencies:
{ "name": "my-inertia-app", "version": "0.1.0", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview" }, "dependencies": { "@inertiajs/react": "^2.0.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "typescript": "^5.5.3", "vite": "^5.4.2" }}Install dependencies:
# Using npmnpm install
# Using bunbun install
# Using pnpmpnpm installStep 2: Configure Vite
Section titled “Step 2: Configure Vite”Create vite.config.ts:
import { defineConfig } from 'vite'import react from '@vitejs/plugin-react'import path from 'path'
export default defineConfig({ plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, './frontend'), }, }, build: { manifest: true, outDir: 'static/build', rollupOptions: { input: 'frontend/app.tsx', }, }, server: { port: 5173, strictPort: true, },})Key settings:
manifest: true- Generates a manifest file for production asset loadingoutDir: 'static/build'- Where built files are placedinput: 'frontend/app.tsx'- Your app’s entry point
Step 3: Configure TypeScript
Section titled “Step 3: Configure TypeScript”Create tsconfig.json:
{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "baseUrl": ".", "paths": { "@/*": ["./frontend/*"] } }, "include": ["frontend"]}Step 4: Create the Inertia App Entry Point
Section titled “Step 4: Create the Inertia App Entry Point”Create frontend/app.tsx:
import { createInertiaApp } from '@inertiajs/react'import { createRoot } from 'react-dom/client'import './globals.css'
// Option 1: Explicit imports (recommended for smaller apps)import Home from './pages/Home'import About from './pages/About'import UsersIndex from './pages/Users/Index'import UsersShow from './pages/Users/Show'
const pages: Record<string, React.ComponentType<any>> = { Home, About, 'Users/Index': UsersIndex, 'Users/Show': UsersShow,}
createInertiaApp({ resolve: (name) => { const page = pages[name] if (!page) { throw new Error(`Page component "${name}" not found`) } return page }, setup({ el, App, props }) { createRoot(el).render(<App {...props} />) },})Dynamic Imports (Alternative)
Section titled “Dynamic Imports (Alternative)”For larger apps, use dynamic imports with import.meta.glob:
import { createInertiaApp } from '@inertiajs/react'import { createRoot } from 'react-dom/client'import './globals.css'
createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('./pages/**/*.tsx', { eager: true }) const page = pages[`./pages/${name}.tsx`] if (!page) { throw new Error(`Page component "${name}" not found`) } return page }, setup({ el, App, props }) { createRoot(el).render(<App {...props} />) },})Step 5: Create the Root HTML Template
Section titled “Step 5: Create the Root HTML Template”Create templates/app.html:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My App</title> {{ vite() | safe }}</head><body> <div id="app" data-page='{{ page | safe }}'></div></body></html>The {{ vite() }} function automatically includes:
- React Fast Refresh scripts (dev mode)
- Vite client scripts (dev mode)
- Built CSS and JS files (production mode)
Using View Data for SEO
Section titled “Using View Data for SEO”For dynamic page titles and meta tags, use view data:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% if page_title %}{{ page_title }}{% else %}My App{% endif %}</title> {% if meta_description %} <meta name="description" content="{{ meta_description }}"> {% endif %} {{ vite() | safe }}</head><body> <div id="app" data-page='{{ page | safe }}'></div></body></html>Step 6: Create Your First Page
Section titled “Step 6: Create Your First Page”Create frontend/pages/Home.tsx:
import { Link } from '@inertiajs/react'
interface HomeProps { message: string}
export default function Home({ message }: HomeProps) { return ( <div> <h1>Welcome</h1> <p>{message}</p> <Link href="/about">Go to About</Link> </div> )}Step 7: Create a Layout Component
Section titled “Step 7: Create a Layout Component”Create frontend/components/Layout.tsx:
import { Link, usePage } from '@inertiajs/react'import React from 'react'
interface LayoutProps { children: React.ReactNode title?: string}
interface SharedProps { auth: { user: { id: number name: string } } flash: { message?: string category?: 'success' | 'error' }}
export default function Layout({ children, title }: LayoutProps) { const { auth, flash } = usePage<SharedProps>().props
return ( <div className="min-h-screen"> <nav className="bg-gray-800 text-white p-4"> <div className="container mx-auto flex justify-between"> <Link href="/" className="font-bold">My App</Link> <div className="flex gap-4"> <Link href="/">Home</Link> <Link href="/about">About</Link> <span>{auth.user.name}</span> </div> </div> </nav>
{flash.message && ( <div className={`p-4 ${flash.category === 'error' ? 'bg-red-100' : 'bg-green-100'}`}> {flash.message} </div> )}
<main className="container mx-auto p-4"> {title && <h1 className="text-2xl font-bold mb-4">{title}</h1>} {children} </main> </div> )}Use the layout in your pages:
import Layout from '@/components/Layout'
export default function Home({ message }: HomeProps) { return ( <Layout title="Home"> <p>{message}</p> </Layout> )}Step 8: Add Global Styles
Section titled “Step 8: Add Global Styles”Create frontend/globals.css:
/* Basic reset */*, *::before, *::after { box-sizing: border-box;}
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.5;}
/* Or import Tailwind CSS *//* @tailwind base;@tailwind components;@tailwind utilities; */Running the Application
Section titled “Running the Application”Development Mode
Section titled “Development Mode”Start both the Vite dev server and FastAPI:
# Terminal 1: Start Vitenpm run dev
# Terminal 2: Start FastAPIuvicorn main:app --reloadOr create a run-dev.sh script:
#!/bin/bash# Start Vite in backgroundnpm run dev &VITE_PID=$!
# Start FastAPIuvicorn main:app --reload
# Cleanup on exittrap "kill $VITE_PID" EXITProduction Build
Section titled “Production Build”Build the frontend assets:
npm run buildThis creates:
static/build/.vite/manifest.json- Asset manifeststatic/build/assets/- Compiled JS and CSS files
Start FastAPI (no Vite needed in production):
uvicorn main:appTypeScript Types
Section titled “TypeScript Types”Page Props Types
Section titled “Page Props Types”Define types for your page components:
export interface User { id: number name: string email: string}
export interface PaginatedResponse<T> { data: T[] total: number page: number per_page: number has_more: boolean}import { User, PaginatedResponse } from '@/types'
interface UsersIndexProps { users: PaginatedResponse<User>}
export default function UsersIndex({ users }: UsersIndexProps) { return ( <div> {users.data.map(user => ( <div key={user.id}>{user.name}</div> ))} </div> )}Shared Props Types
Section titled “Shared Props Types”Type your shared data:
export interface SharedProps { auth: { user: User | null } flash: { message?: string category?: 'success' | 'error' | 'warning' | 'info' } // Add other shared props}import { usePage } from '@inertiajs/react'import { SharedProps } from '@/types'
export default function Layout({ children }) { const { auth, flash } = usePage<SharedProps>().props // ...}Common Patterns
Section titled “Common Patterns”Using Forms
Section titled “Using Forms”import { useForm } from '@inertiajs/react'
export default function ContactForm() { const { data, setData, post, processing, errors } = useForm({ name: '', email: '', message: '', })
const submit = (e: React.FormEvent) => { e.preventDefault() post('/contact') }
return ( <form onSubmit={submit}> <input value={data.name} onChange={e => setData('name', e.target.value)} /> {errors.name && <span>{errors.name}</span>}
<button type="submit" disabled={processing}> Send </button> </form> )}Navigation with router
Section titled “Navigation with router”import { router } from '@inertiajs/react'
// Navigate programmaticallyrouter.visit('/users/1')
// With optionsrouter.visit('/users', { method: 'get', preserveState: true, preserveScroll: true, only: ['users'],})
// POST requestrouter.post('/users', { name: 'John' })
// Reload current pagerouter.reload()Progress Indicator
Section titled “Progress Indicator”Add a loading indicator for navigation:
import { router } from '@inertiajs/react'import { useEffect, useState } from 'react'
export default function Layout({ children }) { const [loading, setLoading] = useState(false)
useEffect(() => { const start = () => setLoading(true) const finish = () => setLoading(false)
router.on('start', start) router.on('finish', finish)
return () => { router.off('start', start) router.off('finish', finish) } }, [])
return ( <div> {loading && <div className="progress-bar" />} {children} </div> )}Next Steps
Section titled “Next Steps”- Configuration - Customize Inertia settings
- Shared Data - Pass data to all pages
- Validation Errors - Handle form errors
- Partial Reloads - Optimize data loading