Table of Contents
Overview
This post explains why Guren, a Laravel-inspired fullstack TypeScript framework running on Bun, uses Hono as its HTTP layer, and what we build on top of it.
Guren uses Hono for HTTP handling, Drizzle ORM for the database, and Inertia.js to connect the frontend. That said, if you're writing application code in Guren, you'll rarely touch Hono's API directly. Guren's Controller and Router sit on top of Hono and hide it from you.
export class PostController extends Controller {
async index() {
const posts = await Post.all()
return this.inertia(pages.posts.Index, { posts })
}
}
This is what a Guren controller looks like. There's a Hono Context running underneath, but you don't need to think about it while writing this.
1. Why Hono
Guren's design principle is "Bun-first, but deploy anywhere." The dev server and test runner use Bun's native APIs directly, so day-to-day development is optimized for Bun. At the same time, we care just as much about being able to deploy to AWS Lambda, Cloudflare Workers, Vercel, or wherever you need in production.
Why Bun over Node.js, then? Simply because it's faster. In a benchmark comparing two identical Inertia SSR apps (framework-comparison), Guren gets 2.3x the throughput on SSR pages, 3.5x on JSON routes, and 1.8x faster cold starts. The app code is identical, so the difference comes down to the runtime.
Hono was built from the start to run identically across runtimes, with Cloudflare Workers as its original target. It's tested against each runtime in CI, and that investment in portability shows. Since it's built on the Web Standard Request/Response, it runs on Cloudflare Workers, Deno, and Bun, as well as Node.js. It works fine on hosting platforms like Vercel too.
Guren actually ships deployment support for AWS Lambda (@guren/plugin-lambda), Cloudflare Workers (@guren/plugin-cloudflare, with D1), and Vercel (@guren/plugin-vercel).
// lambda.ts
import app from './src/app'
import { createLambdaHandler } from '@guren/core/lambda'
await app.boot()
export const handler = createLambdaHandler(app)
Deploying to Lambda comes down to these few lines. Hono's thin abstraction is exactly what makes it easy for us to write these per-runtime adapters ourselves. It's a small thing, but it matters. If Guren's foundation were tied more tightly to Bun-specific behavior, this would have worked against that design principle.
2. Type safety that runs from the database to the frontend
Hono has an RPC feature (HC) that infers types for your path params, query, request body, and response. That gives you a fully type-safe JSON API client. But Hono is a thin HTTP layer, and a database or ORM isn't part of its scope. RPC makes the shape of a JSON request/response type-safe. Where that shape actually comes from — a database schema, or a hand-written type — is outside what Hono is responsible for.
Guren is a batteries-included framework with Drizzle ORM built in from the start, so the starting point for its types can sit further upstream, at the database schema itself. Let's walk through how a type actually flows from that schema all the way to the frontend props.
Database schema
// db/schema.ts
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
body: text('body').notNull(),
})
Model
import { defineModel } from '@guren/orm'
import { posts } from '@/db/schema'
export type PostRecord = typeof posts.$inferSelect
export class Post extends defineModel(posts) {}
Once you build a Model with defineModel(posts), the return type of Post.findOrFail(id) is inferred straight from the posts table schema.
Resource
Think of a Resource as the DTO (Data Transfer Object) pattern. It's a dedicated class that transforms a raw database value into the shape you actually want to send back, instead of exposing it as-is.
// app/Http/Resources/PostResource.ts
import { Resource } from '@guren/core'
import type { PostRecord } from '@/app/Models/Post'
export interface PostResourceData {
id: number
title: string
body: string | null
}
export class PostResource extends Resource<PostRecord> {
toArray(): PostResourceData {
return {
id: this.resource.id,
title: this.resource.title,
body: this.resource.body,
}
}
}
The Post model's type comes from the table definition in db/schema.ts, as typeof posts.$inferSelect. That becomes PostRecord, the input type for PostResource. On the output side, PostResourceData is defined once inside PostResource and then just imported wherever it's needed.
Routing
// routes/web.ts
import PostController from '@/app/Http/Controllers/PostController'
posts.get('/:id', [PostController, 'show']).name('posts.show')
This wires the /:id URL to PostController.show and names the route posts.show. The pages.posts.Show that PostController.show calls via this.inertia(pages.posts.Show, ...) actually comes from the file path resources/js/pages/posts/Show.tsx, not from this route name, but by convention the two are kept in sync.
Validation
// app/Http/Validators/PostValidator.ts
import { z } from 'zod'
export const PostIdParamSchema = z.object({
id: z.coerce.number().int().positive(),
})
Controller
async show() {
const { id } = this.validateParams(PostIdParamSchema)
const post = await Post.findOrFail(id)
return this.inertia(pages.posts.Show, { post: new PostResource(post).toJSON() })
}
id goes through validateParams with a Zod schema (PostIdParamSchema) first, so it's guaranteed to be a number before it ever reaches Post.findOrFail.
View (the page component)
The only thing written by hand here is the PostResourceData interface. The React page component just imports that same type straight from the resource file and uses it as its Props:
// 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 (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
)
}
Running bunx guren codegen parses the Props interface declared in the page with a Babel AST and folds it into .guren/pages.gen.ts. It then checks whatever you pass into this.inertia(pages.posts.Show, { post: ... }) against it at compile time. Passing a page something it doesn't expect fails at build time.
We also generate a typed API client (routes.gen.ts / api-client.gen.ts) for when you're consuming Guren as a JSON API. But Guren's real strength isn't there. It's that the type flows from the database schema all the way to the frontend props, without having to rewrite it at every layer it crosses. That falls outside the scope of a standalone HTTP library like Hono or Elysia, since neither one dictates an ORM or a frontend integration. It's less that Guren is "wider than Hono's RPC" and more that the two cover different ground entirely.
3. AI agent tooling stacks on the same spot
Hono actually has its own AI-agent-oriented CLI, honojs/cli, with commands like docs (browse documentation), search (search documentation), request (test requests), and optimize (bundle optimization). Since Hono itself doesn't dictate an application structure, those commands stay centered on documentation and testing. Guren dictates a structure — the MVC pattern — so it can stack CLI commands that verify and summarize that structure on top.
bunx guren context User # bundles the User model, its routes, pages, resource, policy, and related docs into one
bunx guren check # checks route/controller/page consistency and architecture boundaries
bunx guren audit # security audit: missing validation, missing authorization, raw SQL, leaked secrets
guren context User bundles the User model's definition, its related routes, pages, resource, policy, and related docs into a single Markdown document. When you ask an AI agent to work on anything User-related, instead of having it search the whole codebase every time, you hand it the context it needs with this one command.
guren check mechanically checks whether route definitions, controllers, and Inertia pages line up, and whether a registered route is actually reachable from the registrar — consistency checks that assume the whole framework's structure. guren audit checks whether data-changing routes — POST, PUT, DELETE, and the like — have validation and authorization attached, and scans for raw SQL or leaked secrets.
Neither of these is something Hono on its own is meant to cover. Hono just handles HTTP, so a check like "does this route have authorization attached" or "what files relate to this model" — one that assumes a whole-framework structure — is outside what it's responsible for. It's precisely because Guren dictates the shape of an application that it can stack tools on top that reason about that shape.
4. The hypothesis that conventions help
There's a hypothesis behind this: the more a framework enforces conventions about where things live — the way Rails or Laravel does — the more effective agent guidance like rules and skills becomes. This isn't just my own hunch — both the Rails and Laravel camps say the same thing.
Rails' own Rails and AI page argues that Convention over Configuration is exactly what lets an agent produce idiomatic code from a short prompt. DHH put it this way on X:
Convention over configuration set the path for 20+ years of great training data for AI to use today. Not only does this mean agents do great with Rails, but also that squishy humans can quickly and confidently review the output without a jungle of distracting boilerplate.
Laravel's Taylor Otwell made essentially the same point in an interview on the Laravel blog:
I think it matters in the sense that LLMs still do well with things that are easy to parse and understand. Frameworks that lean into conventions and structure do well with LLMs. There's this very conventional structure to the projects where it's like, there's a models' directory, there's a controllers' directory. It's very discoverable. — Laravel AI SDK, Boost, or MCP: Which Tool Do You Need?
We measured something adjacent for Guren. What we tested wasn't "with conventions vs. without" — it was "with agent guidance (rules, skills, CLI tools) vs. without," on top of the same Guren conventions in both conditions. Across 360 cells (3 models × 20 tasks × with/without the harness × 3 runs), Sonnet 5 shipped with the harness at −28% turns and −25% cost, and guren check got invoked 119 out of 180 runs with the harness versus 15 out of 180 without it. What that shows is that guidance pays off on top of a conventional framework — it doesn't isolate the effect of the conventions themselves. Details are in the full report.
There's research pointing the other way, too. Constraint decay: The Fragility of LLM Agents in Backend Code Generation found that minimal frameworks like Express, Koa, and Flask scored around 50% Assert%, while convention-driven ones like Django and FastAPI dropped to around 25%. The cause: agents can't reliably infer a framework's implicit behavior — automatic validation from type hints, convention-based auto-discovery — from pretraining data alone.
The framework that came in last in that benchmark was, of all things, Hono — supposedly one of the least convention-heavy frameworks in the set — at 18.5%. But by the paper's own account, that's not down to convention overload — it's that setting up the edge-runtime compatibility adapter (@hono/node-server) is thinly represented in training data, a different failure mode entirely. Citing Hono's ranking as evidence that "more convention means worse performance" mixes up two different causes.
The agents in that benchmark were given no framework-specific guidance at all — the result reflects what happens when following convention is left entirely to an LLM's pretraining. Rails and Laravel can lean on "20+ years of public code baked into the training data," but Guren is too young a framework to have that foundation. What it has instead is guren context, guren check, and guren audit, which externalize and verify convention-following mechanically rather than leaving it to the model's implicit knowledge. Guren's answer isn't to thin out its conventions, and it isn't to wait for those conventions to get baked into training data — it's to make the conventions checkable by tooling.
Conclusion
To sum up, in my own opinion:
- Guren picked Hono as its foundation because it fit our "not locked into Bun, deployable to Lambda and Cloudflare Workers" design goal
- Guren adds end-to-end type safety that runs from the database schema all the way to Inertia props, at the framework layer
- Because Guren dictates a structure — the MVC pattern — it can also stack AI agent tooling like
guren context,guren check, andguren auditon top - Hono's thin abstraction is what keeps Guren's own runtime adapters simple to write
Building on a minimal library like Hono puts the real design decisions on what you stack on top of it. With Guren, the goal is to absorb those foundational and runtime-specific differences at the framework layer, so the person writing the app can just focus on business logic.
If you're curious, take a look at the Guren repository.