tpl-backend-hono-bun
Template do pack (backend/08-hono-bun.md). Orienta o agente em APIs, servicos e arquitetura backend alinhado a esse contexto.
Files1
View on GitHubSKILL.mdfavorites/tpl-backend-hono-bun/SKILL.md
GitHubPROJECT: Hono + Bun REST API
Skill gerado a partir do pack templates-claude-code. Arquivo de origem: backend/08-hono-bun.md. Use como baseline e adapte ao projeto antes de mudancas grandes.
Conteudo do template
STACK
- Runtime: Bun 1.x
- Framework: Hono v4
- Language: TypeScript 5.x (strict)
- ORM: Drizzle ORM
- Database: SQLite (local) / Turso (production)
- Validation: Zod
- Testing: Vitest + @hono/testing
- Deployment: Cloudflare Workers / Fly.io edge
PROJECT STRUCTURE
src/
├── index.ts # App entry, route mounting
├── env.ts # Zod-validated env vars
├── db/
│ ├── index.ts # Drizzle client + connection
│ ├── schema.ts # All table definitions
│ └── migrations/ # Drizzle generated migrations
├── routes/
│ ├── users.ts
│ ├── posts.ts
│ └── auth.ts
├── middleware/
│ ├── auth.ts # JWT verification
│ ├── logger.ts
│ └── rate-limit.ts
├── lib/
│ ├── errors.ts # Custom error classes
│ └── response.ts # Typed response helpers
└── types.ts # Env + Variables types
tests/
├── routes/
│ └── users.test.ts
└── helpers/
└── app.ts # Test app factory
ARCHITECTURE RULES
- Every route file exports a
Honoinstance — never export raw handlers. - Use
c.varfor typed middleware state — define inVariablestype; never usec.setwith untyped keys. - Zod validates all inputs — body via
zValidator, query viazValidator('query', ...). - Drizzle schema = single source of truth — never use raw SQL except for complex aggregates.
- RPC mode is required — export
AppTypefromindex.tsfor type-safe client usage. - No
anytypes — useunknown+ zod parse at the boundary. - Environment validated at startup — app does not start with missing env vars.
MIDDLEWARE PATTERN
// src/types.ts
import type { Context } from 'hono'
import type { User } from './db/schema'
export type Variables = {
user: User
requestId: string
}
export type Env = {
Variables: Variables
Bindings: {
DATABASE_URL: string
JWT_SECRET: string
}
}
// src/middleware/auth.ts
import { createMiddleware } from 'hono/factory'
import { HTTPException } from 'hono/http-exception'
import { verify } from 'hono/jwt'
import { db } from '../db'
import { users } from '../db/schema'
import { eq } from 'drizzle-orm'
import type { Env } from '../types'
export const authMiddleware = createMiddleware<Env>(async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) throw new HTTPException(401, { message: 'No token provided' })
const payload = await verify(token, c.env.JWT_SECRET).catch(() => {
throw new HTTPException(401, { message: 'Invalid token' })
})
const user = await db.select().from(users).where(eq(users.id, payload.sub as string)).get()
if (!user) throw new HTTPException(401, { message: 'User not found' })
c.set('user', user)
await next()
})
DRIZZLE SCHEMA + QUERIES
// src/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
import { createId } from '@paralleldrive/cuid2'
export const users = sqliteTable('users', {
id: text('id').primaryKey().$defaultFn(() => createId()),
email: text('email').notNull().unique(),
name: text('name').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
})
export const posts = sqliteTable('posts', {
id: text('id').primaryKey().$defaultFn(() => createId()),
title: text('title').notNull(),
body: text('body').notNull(),
authorId: text('author_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
})
// src/db/index.ts
import { drizzle } from 'drizzle-orm/bun-sqlite'
import { Database } from 'bun:sqlite'
import * as schema from './schema'
const sqlite = new Database(process.env.DATABASE_URL ?? 'dev.db')
export const db = drizzle(sqlite, { schema })
ROUTE + RPC MODE
// src/routes/users.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { db } from '../db'
import { users } from '../db/schema'
import { eq } from 'drizzle-orm'
import { authMiddleware } from '../middleware/auth'
import type { Env } from '../types'
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
})
export const usersRouter = new Hono<Env>()
.get('/', authMiddleware, async (c) => {
const all = await db.select().from(users).all()
return c.json({ data: all })
})
.post('/', zValidator('json', createUserSchema), async (c) => {
const body = c.req.valid('json')
const user = await db.insert(users).values(body).returning().get()
return c.json({ data: user }, 201)
})
.get('/:id', authMiddleware, async (c) => {
const user = await db.select().from(users).where(eq(users.id, c.req.param('id'))).get()
if (!user) return c.json({ error: 'Not found' }, 404)
return c.json({ data: user })
})
// src/index.ts
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { usersRouter } from './routes/users'
const app = new Hono()
.use('*', logger())
.use('*', cors())
.route('/api/users', usersRouter)
// RPC-safe export
export type AppType = typeof app
export default app
HONO RPC CLIENT
// client.ts (frontend/other service)
import { hc } from 'hono/client'
import type { AppType } from './src/index'
const client = hc<AppType>('http://localhost:3000')
// Fully typed — no manual types needed
const res = await client.api.users.$get()
const { data } = await res.json()
// ^? User[]
ROUTING TABLE
| Method | Path | Middleware | Action |
|---|---|---|---|
| GET | /api/users | auth | List all users |
| POST | /api/users | zod body | Create user |
| GET | /api/users/:id | auth | Get user by ID |
| PUT | /api/users/:id | auth + zod | Update user |
| DELETE | /api/users/:id | auth | Delete user |
| POST | /api/auth/login | zod body | Issue JWT |
| POST | /api/auth/refresh | — | Refresh token |
| GET | /api/posts | — | Public post list |
| POST | /api/posts | auth + zod | Create post |
| GET | /health | — | Liveness probe |
QUALITY GATES
-
bun tsc --noEmitpasses with zero errors - All route inputs validated by Zod (
zValidator) -
AppTypeexported — client is fully type-safe - No
anycasts in route handlers - Every protected route uses
authMiddleware - All env vars validated in
env.tsat startup - Drizzle migrations committed and up-to-date
-
bun testpasses with ≥80% coverage on routes - Error responses follow
{ error: string }shape consistently
FORBIDDEN
- ❌ Raw
fetchinside route handlers — create a service layer - ❌
c.set('user', data as any)— always typeVariables - ❌ Mutations without Zod validation on input
- ❌
process.env.Xwithout theenv.tsguard - ❌ Returning raw Drizzle error messages to the client
- ❌ Embedding business logic directly in route handlers
- ❌
console.login production code — use structured logger