First Steps: A 10-Minute Tour of One Request
This tour follows a single request — GET /posts — through every layer of a Guren app: route, controller, validation, model, resource, Inertia page, and test. Read it to build a mental map; each stop links to the guide that goes deeper.
First Steps: A 10-Minute Tour of One Request
This tour follows a single request — GET /posts — through every layer of a Guren app: route, controller, validation, model, resource, Inertia page, and test. Read it to build a mental map; each stop links to the guide that goes deeper.
It assumes a running app (see Getting Started) with a posts resource generated by:
bunx guren add resource posts --fields "title:string,body:text,published:boolean"
Prefer to build this yourself step by step? Take the Build a Mini Blog tutorial instead — it covers the same ground hands-on.
1. The route
Every request starts in routes/web.ts, where a registrar maps URLs to controller actions:
import { Router } from '@guren/core'
import PostController from '@/app/Http/Controllers/PostController'
export function registerWebRoutes(router: Router): void {
router.get('/posts', [PostController, 'index'])
router.post('/posts', [PostController, 'store'])
}
GET /posts matches the first line, so Guren dispatches to PostController.index. Groups, middleware, and named routes all live here too — see the Routing Guide.
2. The controller
app/Http/Controllers/PostController.ts handles the request:
import { Controller } from '@guren/core'
import { Post } from '@/app/Models/Post'
import { PostResource } from '@/app/Http/Resources/PostResource'
import { ListPostsQuerySchema } from '@/app/Http/Validators/PostValidator'
import { pages } from '@/.guren/pages.gen'
export default class PostController extends Controller {
async index() {
const { page } = this.validateQuery(ListPostsQuerySchema)
const result = await Post.paginate({ page, perPage: 20 })
return this.inertia(pages.posts.Index, {
data: result.data.map((post) => new PostResource(post).toJSON()),
})
}
}
Three things happen: input is validated, data is fetched, and a page is rendered. The Controller Guide covers the full toolbox.
3. Validation
this.validateQuery(schema) parses the query string against a Zod schema and throws a 422 automatically on bad input — no manual error handling:
import { z } from 'zod'
export const ListPostsQuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
})
validateBody and validateParams work the same way for request bodies and route parameters. See the Validation Guide.
4. The model
app/Models/Post.ts binds a class to a Drizzle table from db/schema.ts:
import { Model } from '@guren/orm'
import { posts } from '@/db/schema'
export class Post extends Model<typeof posts> {
static table = posts
}
Queries read like Laravel: Post.find(1), Post.findOrFail(1) (throws a 404), Post.where('published', true).get(). Column types flow from the schema into every result. See the Database Guide.
5. The resource
app/Http/Resources/PostResource.ts decides what leaves the server — so internal columns never leak by accident:
import { Resource } from '@guren/core'
export class PostResource extends Resource<Post> {
toArray() {
const { id, title, body, published } = this.resource
return { id, title, body, published }
}
}
See the API Resources Guide.
6. The Inertia page
this.inertia(pages.posts.Index, props) renders resources/js/pages/posts/Index.tsx — a plain React component that receives the controller's props directly, no API layer in between:
import type { PageProps } from '@guren/inertia-client/contracts'
import { pages } from '@/.guren/pages.gen'
type Props = PageProps<typeof pages.posts.Index>
export default function PostsIndex({ data }: Props) {
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Codegen extracts each page's Props into .guren/pages.gen.ts, so a controller passing the wrong shape is a compile error — rename a column and TypeScript flags every layer from schema to browser. See the Frontend Guide.
7. The test
Prove the whole path works with TestApp, which drives real requests through your booted app:
import { test } from 'bun:test'
import { TestApp } from '@guren/testing'
test('lists posts', async () => {
const app = await TestApp.create()
await app.get('/posts').assertOk()
})
See the Testing Guide for fluent assertions, actingAs, and database helpers.
The mental model
Every feature in a Guren app follows this same path:
- routes map URLs to controllers
- validators parse inputs
- models describe data access
- resources shape outputs
- pages define props
- controllers compose it all
To add a feature, scaffold the whole path at once and refresh the manifests:
bunx guren add resource comments --fields "body:text,postId:integer"
bun run codegen
Where to go next
Ready to build for real? The Build a Mini Blog tutorial walks you through posts, authentication, and comments hands-on. If any term was unfamiliar, check the Glossary.