Guide/guides

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 dev boots the app.
  • Use VITE_DEV_SERVER_URL only 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 seed GUREN_INERTIA_ENTRY/GUREN_INERTIA_STYLES in production.
  • Locate the SSR manifest (public/assets/.vite/ssr-manifest.json) and set GUREN_INERTIA_SSR_ENTRY / GUREN_INERTIA_SSR_MANIFEST so 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 │
└──────────────┘                        └──────────────────┘
  1. Define Props in the page component — each page declares an interface Props describing 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>
}
  1. Codegen extracts Props — running bun run codegen (or automatically during bun run dev) scans every page component, extracts the interface 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']>(...)
  }
}
  1. Controller gets type-checked — when a controller calls this.inertia(pages.posts.Show, { ... }), TypeScript checks the second argument against the PageContract'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 tsconfig paths and in the frontend build via the Guren Vite plugin.
  • Run bun run codegen after adding or changing Props to keep pages.gen.ts up 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.