Six Months with Hono and Elysia: The Pitfalls I Hit
Hono and Elysia are the two hottest TypeScript web frameworks right now: one focuses on being lightweight and cross-platform, the other on raw performance. After six months with them, here are the pitfalls I ran into in real projects.
Introduction
Bottom line first: Hono and Elysia are both excellent frameworks, with great performance and strong type support. But "excellent" doesn't mean "perfect", and in real projects some design choices will trip you up.
This post isn't meant to bash either framework; it's here to help you avoid the pitfalls up front.
Part 1: Hono Pitfalls
Pitfall 1: Wildcard Route Matching Is Unintuitive
app.get('/api/*', handler)
// ✅ matches /api/users
// ❌ doesn't match /api
// want to match both? write it twice
app.get('/api', handler)
app.get('/api/*', handler)Pitfall 2: c.set() Types Must Be Declared Up Front
// must be declared in the generic
type Env = { Variables: { user: User } }
const app = new Hono<Env>()
// across files it gets messy...Pitfall 3: Path Params Are Always Strings
const id = c.req.param('id') // always a string
const numId = parseInt(id) // convert every timePitfall 4: Validation Needs an Extra Package and Is Verbose
import { zValidator } from '@hono/zod-validator'
app.post('/users',
zValidator('json', schema),
(c) => {
const body = c.req.valid('json') // you need .valid()
}
)Pitfall 5: RPC Client Type Inference Has Gaps
const res = await client.users.$get()
const data = await res.json() // and you still call .json() manually
// POST body type inference is inaccuratePitfall 6: Inconsistent Error Handling
HTTPException and directly returned responses have different formats, and teams end up without a standard.
Part 2: Elysia Pitfalls
Pitfall 1: Chains Get Too Long
const app = new Elysia()
.state('version', '1.0.0')
.decorate('logger', new Logger())
.derive(({ headers }) => ({ auth: headers.authorization }))
.onBeforeHandle(...)
.onAfterHandle(...)
.get('/users', ...)
.post('/users', ...)
.listen(3000)
// 50 lines of chaining; finding a route takes foreverPitfall 2: Too Many Concepts
state- global statedecorate- inject utilitiesderive- derive per requestresolve- derive after validation
Newcomers need a long time to understand them.
Pitfall 3: Nested guard Hell
.guard({}, app => app
.guard({}, app => app
.guard({}, app => app
.post('/resource', handler)
)
)
)
// terrifying indentation levelsPitfall 4: Plugin Types Require use() on the Chain
You can't extract plugin configuration into a separate file for reuse.
Pitfall 5: Type Inference Breaks When You Split Files
// routes/users.ts
export const userRoutes = new Elysia()
.get('/users', ({ db }) => {
// ❌ db doesn't exist, because it was decorated on the main app
})Pitfall 6: Bun Only
- ❌ Node.js
- ❌ Cloudflare Workers
- ❌ Vercel Edge
- ❌ AWS Lambda
Choosing Elysia locks you into a runtime.
Pitfall 7: Error Handling Requires Understanding the Lifecycle
There are 7 lifecycle hooks, and you need the docs to figure out how they behave.
Part 3: Solutions
Working Around Hono
- Wildcard routes: write them twice or use a regex
- Type declarations: define a global Env type
- Validation: use @hono/zod-validator
Working Around Elysia
- Long chains: split files and compose with .use()
- Many concepts: invest time in learning them
- Runtime lock-in: make sure your team supports Bun
Or Consider Another Approach
Declarative routing:
const routes = defineRoutes([
defineRoute({ method: 'GET', path: '/users', handler: getUsers }),
defineRoute({ method: 'POST', path: '/users', handler: createUser, middleware: [auth] }),
])Advantages:
- Routes are visible at a glance
- Middleware is declared explicitly
- Types don't get lost across files
- Not tied to a runtime
Summary
| Framework | Strengths | Pitfalls |
|---|---|---|
| Hono | Lightweight, cross-platform | Wildcard rules, verbose type declarations |
| Elysia | Top performance | Chaining hell, Bun lock-in |
Recommendations:
- Small projects → Hono
- Top performance + Bun → Elysia
- Large projects + clear structure → a declarative framework
- Cross-runtime → Hono or a declarative framework
Are you using Hono or Elysia? What pitfalls have you hit? Let's talk!