Frontend Guide
Guren delivers a single-page application experience by combining Inertia.js with React. Controllers return Inertia responses, and the frontend renders the matching React components located under resources/js/pages/.
Frontend Guide
Guren delivers a single-page application experience by combining Inertia.js with React. Controllers return Inertia responses, and the frontend renders the matching React components located under resources/js/pages/.
Project Structure
resources/js/app.tsx: Bootstraps the Inertia app and registers global providers.resources/js/ssr.tsx: Exports the server-side renderer consumed by the backend when SSR is enabled.resources/js/pages/: React components that map to controller responses.resources/js/components/: Shared UI components (optional but recommended).resources/css/app.css: Tailwind (or your chosen CSS) entry point.
Page Components
Page filenames map to the page definitions auto-generated in .guren/pages.gen.ts. Props are defined as interface Props in each page component and extracted by codegen:
// Controller
return this.inertia(pages.posts.Index, {
data,
pagination,
})
// resources/js/pages/posts/Index.tsx
import type { PageProps } from '@guren/inertia-client/contracts'
import { Head, Link } from '@inertiajs/react'
import { pages } from '@/.guren/pages.gen'
type Props = PageProps<typeof pages.posts.Index>
export default function Index({ data, pagination }: Props) {
return (
<>
<Head title="Posts" />
<div className="space-y-4">
{data.map((post) => (
<article key={post.id} className="rounded border border-slate-200 p-4">
<h2 className="text-lg font-semibold">{post.title}</h2>
<p className="text-slate-600">{post.excerpt}</p>
<Link className="text-blue-600 underline" href={`/posts/${post.id}`}>
Read more
</Link>
</article>
))}
</div>
<p className="mt-4 text-sm text-slate-500">{pagination.meta.total} posts</p>
</>
)
}
Use TypeScript to annotate props so you benefit from compile-time safety.
Layouts and Shared UI
Wrap pages with layout components to keep navigation and shared UI consistent:
// resources/js/components/Layout.tsx
export function Layout({ children }: React.PropsWithChildren) {
return (
<div className="min-h-screen bg-slate-50">
<header className="border-b bg-white">
<div className="mx-auto flex max-w-4xl items-center justify-between px-6 py-4">
<a href="/">Guren</a>
</div>
</header>
<main className="mx-auto max-w-4xl px-6 py-10">{children}</main>
</div>
)
}
// resources/js/pages/posts/Index.tsx
import { Layout } from '@/resources/js/components/Layout'
export default function Index({ posts }: Props) {
return (
<Layout>
{/* page content */}
</Layout>
)
}
Forms and Navigation
Inertia provides helpers for client-side navigation and form submissions:
<Link href="/posts/new">Create Post</Link>for navigation without a full reload.const form = useForm({ title: '', body: '' })to manage form state.form.post('/posts')to submit data.
Handle validation errors by returning them from the controller and reading form.errors on the client.
Assets and Styling
The scaffold ships with Tailwind CSS preconfigured. Edit resources/css/app.css or add custom CSS frameworks as needed. If you introduce additional assets (images, fonts), place them under public/.
Server-Side Rendering
Each application ships with a default resources/js/ssr.tsx entry that calls renderInertiaServer() from @guren/inertia-client. When you bootstrap the app with autoConfigureInertiaAssets(app, { importMeta }), Guren will:
- Start and manage a Vite dev server automatically during development when
bun run devboots the app. - Use
VITE_DEV_SERVER_URLonly when you explicitly want to point HTML responses at an already-running external Vite dev server. - Detect the built client manifest (
public/assets/.vite/manifest.json) and automatically seedGUREN_INERTIA_ENTRY/GUREN_INERTIA_STYLESin production. - Locate the SSR manifest (
public/assets/.vite/ssr-manifest.json) and setGUREN_INERTIA_SSR_ENTRY/GUREN_INERTIA_SSR_MANIFESTso Inertia can render on the server.
To produce the required assets run the app build, which runs codegen before the Vite client and SSR builds:
bun run build
You can override the default resolver—useful for custom component lookups—by editing resources/js/ssr.tsx and passing a different resolve function to renderInertiaServer(). If you opt out of autoConfigureInertiaAssets, make sure you populate the required environment variables before calling configureInertiaAssets yourself.
Type Safety
Guren provides end-to-end type safety between controllers and page components through an automatic codegen pipeline.
How the Type Flow Works
Page component codegen Controller
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ interface Props │ pages.gen.ts │ │ this.inertia( │
│ { │ ──►│ PagePropsMap │───►│ pages.posts.Show│
│ post: Post │ │ PageContract │ │ { post } │
│ } │ └──────────────┘ │ ) // type-checked │
└──────────────┘ └──────────────────┘
- Define Props in the page component — each page declares an
interface Propsdescribing the data it expects:
// resources/js/pages/posts/Show.tsx
import type { PostResourceData } from '@/app/Http/Resources/PostResource'
interface Props {
post: PostResourceData
}
export default function Show({ post }: Props) {
return <h1>{post.title}</h1>
}
- Codegen extracts Props — running
bun run codegen(or automatically duringbun run dev) scans every page component, extracts theinterface Props, and writes them into.guren/pages.gen.ts:
// .guren/pages.gen.ts (auto-generated)
export interface PagePropsMap {
'posts/Show': { post: PostResourceData }
}
export const pages = {
posts: {
Show: defineGeneratedPage<'posts/Show', PagePropsMap['posts/Show']>(...)
}
}
- Controller gets type-checked — when a controller calls
this.inertia(pages.posts.Show, { ... }), TypeScript checks the second argument against thePageContract's embedded props type. Missing or mistyped props cause a compile error:
// app/Http/Controllers/PostController.ts
import { pages } from '@/.guren/pages.gen'
export default class PostController extends Controller {
async show() {
const post = await Post.findOrFail(id)
// ✅ Type-checked: { post } must match Props from Show.tsx
return this.inertia(pages.posts.Show, { post: new PostResource(post).toJSON() })
}
}
Using Local Types in Props
Props can reference locally defined types. Codegen automatically collects them:
type Author = { id: number; name: string }
interface Props {
post: { title: string; author: Author }
}
Both Author and the Props body are extracted into pages.gen.ts.
Using Imported Types in Props
Types imported from Resource files or other modules are also tracked:
import type { PostResourceData } from '@/app/Http/Resources/PostResource'
interface Props {
post: PostResourceData
}
Codegen rewrites the import path so pages.gen.ts can reference the same type.
Tips
- Share types between backend and frontend by re-exporting the Drizzle-inferred types from models (e.g.
export type PostRecord = typeof posts.$inferSelect). - Use the
@/alias (the project root) instead of long relative imports — it resolves in server code via tsconfigpathsand in the frontend build via the Guren Vite plugin. - Run
bun run codegenafter adding or changing Props to keeppages.gen.tsup to date.
Hot Reloading
Running bun run dev keeps the frontend and backend in sync—the Bun process automatically launches the Vite dev server, so changes to TSX files trigger instant reloads without extra commands. If you need to customize that workflow, import startViteDevServer() from @guren/core/runtime and manage the Vite instance yourself.
By structuring your pages and components with these patterns, you get a smooth SPA experience with minimal boilerplate, powered entirely by React and Inertia.