The AdonisJS Starter Kit uses React , Inertia.js , and TypeScript for building interactive frontend experiences with server-side rendering (SSR) support.
Tech Stack
React 19 Modern React with hooks and concurrent features
Inertia.js Build SPAs without building an API
TypeScript Type-safe frontend development
Vite Lightning-fast HMR and builds
ShadCN UI Beautiful, accessible components
Tailwind CSS Utility-first CSS framework
Project Structure
Frontend code is organized by feature modules:
apps/web/app/
├── auth/
│ └── ui/
│ ├── pages/ # Inertia pages
│ ├── components/ # Feature components
│ └── hooks/ # Custom React hooks
├── users/
│ └── ui/
│ ├── pages/
│ ├── components/
│ └── context/
├── marketing/
│ └── ui/
└── common/
└── ui/
├── components/ # Shared components
├── hooks/
└── icons/
Each feature module contains its own UI code, keeping frontend logic co-located with backend code.
Inertia.js Configuration
Inertia.js bridges your AdonisJS backend with React frontend:
import { defineConfig } from '@adonisjs/inertia'
import type { InferSharedProps } from '@adonisjs/inertia/types'
const inertiaConfig = defineConfig ({
rootView: 'inertia_layout' ,
sharedData: {
locale : ( ctx ) => ctx . inertia . always (() =>
ctx . i18n ?. locale || i18nManager . config . defaultLocale
),
user : async ( ctx ) => {
if ( ctx . auth ?. user ) {
await User . preComputeUrls ( ctx . auth ?. user )
return new UserDto ( ctx . auth ?. user )
}
},
flashMessages : ( ctx ) => ctx . session ?. flashMessages . all (),
abilities : ( ctx ) => {
if ( ! ctx . auth ?. user ) return []
return new AbilitiesService (). getAllAbilities ( ctx . auth ?. user )
},
},
ssr: {
enabled: true ,
entrypoint: 'app/core/ui/app/ssr.tsx' ,
pages : ( _ , page ) => isSSREnableForPage ( page ),
},
})
export default inertiaConfig
Shared data is automatically available to all Inertia pages without prop drilling.
Creating Pages
Inertia pages are React components that are rendered by your AdonisJS controllers.
Simple Page Example
app/auth/ui/pages/sign_in.tsx
import { LoginForm } from '#auth/ui/components/login_form'
import AuthLayout from '#auth/ui/components/layout'
export default function SignInPage () {
return (
< AuthLayout >
< LoginForm />
</ AuthLayout >
)
}
Controller Rendering
app/auth/controllers/sign_in_controller.ts
import { HttpContext } from '@adonisjs/core/http'
export default class SignInController {
async show ({ inertia } : HttpContext ) {
return inertia . render ( 'auth/sign_in' )
}
}
Inertia provides a powerful form helper for handling form state and submissions.
Here’s the actual login form from the starter kit:
app/auth/ui/components/login_form.tsx
import { useForm } from '@inertiajs/react'
import { Link } from '@tuyau/inertia/react'
import { Button } from '@workspace/ui/components/button'
import { Input } from '@workspace/ui/components/input'
import { PasswordInput } from '@workspace/ui/components/password-input'
import {
FieldSet ,
FieldGroup ,
Field ,
FieldLabel ,
} from '@workspace/ui/components/field'
import { FieldErrorBag } from '@workspace/ui/components/field-error-bag'
export function LoginForm () {
const { data , setData , errors , post } = useForm ({
email: '' ,
password: '' ,
})
function handleSubmit ( e : React . FormEvent ) {
e . preventDefault ()
post ( '/login' )
}
return (
< form onSubmit = { handleSubmit } className = "flex flex-col gap-6" >
< FieldSet >
< FieldGroup >
< Field >
< FieldLabel htmlFor = "email" > Email </ FieldLabel >
< Input
id = "email"
type = "email"
value = { data . email }
onChange = { ( e ) => setData ( 'email' , e . target . value ) }
placeholder = "[email protected] "
required
/>
< FieldErrorBag errors = { errors } field = "email" />
</ Field >
< Field >
< FieldLabel htmlFor = "password" > Password </ FieldLabel >
< PasswordInput
id = "password"
value = { data . password }
onChange = { ( e ) => setData ( 'password' , e . target . value ) }
placeholder = "Enter your password"
required
/>
< FieldErrorBag errors = { errors } field = "password" />
</ Field >
< Field orientation = "responsive" >
< Button type = "submit" > Sign In </ Button >
</ Field >
</ FieldGroup >
</ FieldSet >
</ form >
)
}
The useForm hook automatically handles form state, validation errors, and loading states.
Navigation
Use the type-safe Tuyau router for navigation:
import { Link , useTuyau } from '@tuyau/inertia/react'
// Link component
< Link route = "auth.sign_in.show" >
Sign In
</ Link >
// Programmatic navigation
function MyComponent () {
const tuyau = useTuyau ()
const navigate = () => {
tuyau . navigate ( 'users.index' )
}
return < button onClick = { navigate } > Go to Users </ button >
}
Tuyau provides full TypeScript autocomplete for your routes and parameters.
Using Shared Data
Access shared data (user, locale, etc.) via usePage:
import { usePage } from '@inertiajs/react'
function Header () {
const { user , locale } = usePage (). props
return (
< header >
< p > Welcome, { user ?. fullName } </ p >
< p > Locale: { locale } </ p >
</ header >
)
}
Server-Side Rendering (SSR)
The starter kit includes SSR support for improved performance and SEO.
SSR Configuration
SSR is configured in config/inertia.ts:
ssr : {
enabled : true ,
entrypoint : 'app/core/ui/app/ssr.tsx' ,
pages : ( _ , page ) => isSSREnableForPage ( page ),
}
Building for Production
Build frontend
Build with SSR
Internationalization (i18n)
The starter kit includes built-in i18n support:
import { useTranslation } from '#common/ui/hooks/use_translation'
function MyComponent () {
const { t } = useTranslation ()
return (
< div >
< h1 > { t ( 'auth.signin.title' ) } </ h1 >
< p > { t ( 'auth.signin.description' ) } </ p >
</ div >
)
}
Custom Hooks
The starter kit includes several useful hooks:
useFlashMessage
import useFlashMessage from '#common/ui/hooks/use_flash_message'
function MyComponent () {
const successMessage = useFlashMessage ( 'success' )
const errorMessages = useFlashMessage ( 'errorsBag' )
return (
< div >
{ successMessage && < div className = "success" > { successMessage } </ div > }
{ errorMessages && < div className = "error" > { errorMessages } </ div > }
</ div >
)
}
useUser
import { useUser } from '#auth/ui/hooks/use_user'
function MyComponent () {
const { user , isAdmin } = useUser ()
return (
< div >
{ isAdmin && < AdminPanel /> }
</ div >
)
}
Development Workflow
Start the dev server
This starts both the AdonisJS server and Vite dev server with HMR.
Edit your components
Make changes to your React components in app/*/ui/ directories.
Hot Module Replacement
Changes are reflected instantly thanks to Vite’s HMR.
Type Safety
The starter kit provides full TypeScript support:
Shared props types
Page props
import type { SharedProps } from '@adonisjs/inertia/types'
function MyComponent () {
const { user } = usePage < SharedProps >(). props
// user is fully typed!
}
Best Practices
Pages should be thin wrappers that compose components. Put business logic in hooks and context providers.
Keep feature-specific UI code in the same module as the backend code. Only truly shared components go in common/ui.
Use the Inertia form helper
The useForm hook handles all form state, validation, and loading states automatically. Don’t manage this manually.
Put data needed across many pages in the Inertia shared data config instead of passing it through every controller.
Next Steps
UI Components Learn about the ShadCN UI component library
Routes Understand the routing system