Hono Integration

Learn how to build powerful APIs with Hono in Bini.js — file-based routing, middleware, and type safety.

Hono is a fast, lightweight web framework that works everywhere. Bini.js integrates Hono seamlessly with file-based API routing — your file structure defines your API routes.

Hono is the recommended approach for complex APIs in Bini.js. It provides routing, middleware, validation, and excellent TypeScript support — all with zero-config file-based routing.

File-Based API Routing

Your API route is determined by the file path inside src/app/api/. The file name becomes the route segment:

File PathAPI Route
src/app/api/hello.ts/api/hello
src/app/api/user.ts/api/user
src/app/api/posts.ts/api/posts
src/app/api/posts/[id].ts/api/posts/:id
src/app/api/[...catch].ts/api/*
There are no root / API routes. Every API file maps to a named route based on its filename. Write your Hono routes without the /api prefix — bini-router strips it in dev/preview and mounts the app under /api in production.

Basic Hono App

Create a Hono app in src/app/api/ and default export it:

src/app/api/hello.ts
// src/app/api/hello.ts → /api/hello
import { Hono } from 'hono'

const app = new Hono()

app.all('/hello', (c) => {
  return c.json({
    message  : 'Hello from Bini.js!',
    timestamp: new Date().toISOString(),
    method   : c.req.method,
  })
})

export default app

Routing with Hono

Hono provides a powerful routing system with path parameters, query parameters, and more:

src/app/api/users.ts
// src/app/api/users.ts → /api/users
import { Hono } from 'hono'

const app = new Hono()

app.get('/users', (c) => c.json({ users: ['alice', 'bob'] }))
app.get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
app.post('/users', async (c) => c.json({ created: await c.req.json() }, 201))
app.put('/users/:id', async (c) => c.json({ id: c.req.param('id'), ...await c.req.json() }))
app.delete('/users/:id', (c) => c.json({ message: `Deleted ${c.req.param('id')}` }))

export default app
MethodRoute PatternFull URL
GET/users/api/users
GET/users/:id/api/users/123
POST/users/api/users
PUT/users/:id/api/users/123
DELETE/users/:id/api/users/123

Dynamic API Routes

Use [param] in filenames for dynamic segments:

src/app/api/posts/[id].ts
// src/app/api/posts/[id].ts → /api/posts/:id
import { Hono } from 'hono'

const app = new Hono()
app.get('/posts/:id', (c) => c.json({ id: c.req.param('id') }))
export default app
src/app/api/[...catch].ts
// src/app/api/[...catch].ts → /api/*
import { Hono } from 'hono'

const app = new Hono()
app.all('*', (c) => c.json({ path: c.req.path }))
export default app

Middleware

Hono has built-in middleware for common tasks:

src/app/api/secure.ts
// src/app/api/secure.ts → /api/secure
import { Hono } from 'hono'
import { cors, logger, jwt, timeout } from 'hono/middleware'

const app = new Hono()

app.use('*', cors())
app.use('*', logger())
app.use('*', timeout(5000))

app.get('/secure', (c) => c.json({ message: 'Public' }))

export default app
MiddlewarePurpose
corsCross-Origin Resource Sharing
loggerRequest logging
jwtJWT authentication
timeoutRequest timeout
prettyJSONPretty JSON responses

Request Handling

Hono provides convenient methods for accessing request data:

src/app/api/echo.ts
// src/app/api/echo.ts → /api/echo
import { Hono } from 'hono'

const app = new Hono()

app.all('/echo', async (c) => {
  const params = c.req.param()
  const query = c.req.query()
  const page = c.req.query('page')
  const userAgent = c.req.header('User-Agent')
  const body = await c.req.json().catch(() => null)
  
  return c.json({
    method: c.req.method,
    path: c.req.path,
    params,
    query: { page, ...query },
    headers: { userAgent },
    body,
  })
})

export default app

Response Handling

Hono provides flexible response methods:

src/app/api/responses.ts
// src/app/api/responses.ts → /api/responses
import { Hono } from 'hono'

const app = new Hono()

app.get('/json', (c) => c.json({ message: 'Hello' }))
app.get('/text', (c) => c.text('Hello Text'))
app.get('/html', (c) => c.html('<h1>Hello</h1>'))
app.get('/redirect', (c) => c.redirect('https://example.com', 302))
app.post('/created', (c) => c.json({ message: 'Created' }, 201))
app.get('/error', (c) => c.json({ error: 'Error' }, 500))

export default app

Validation

Validate incoming requests with Zod:

src/app/api/posts.ts
// src/app/api/posts.ts → /api/posts
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

const app = new Hono()

const postSchema = z.object({
  title: z.string().min(1).max(100),
  content: z.string().min(1),
})

app.post('/posts', zValidator('json', postSchema), async (c) => {
  const body = c.req.valid('json')
  return c.json({ post: { id: Date.now(), ...body } }, 201)
})

export default app
Install zod and @hono/zod-validator for powerful request validation with TypeScript inference.

Environment Variables

Use getEnv() and requireEnv() from bini-env:

src/app/api/config.ts
// src/app/api/config.ts → /api/config
import { Hono } from 'hono'

const app = new Hono()

app.get('/config', (c) => {
  const ctx = c as any
  const apiKey = requireEnv(ctx, 'MY_API_KEY')
  const appName = getEnv(ctx, 'APP_NAME') ?? 'Bini.js'
  
  return c.json({ appName, hasApiKey: !!apiKey })
})

export default app
Cast c once at the top of your handler with const ctx = c as any. Then use requireEnv(ctx, 'KEY') for required vars and getEnv(ctx, 'KEY') ?? 'default' for optional ones.

Error Handling

Handle errors gracefully with Hono's error handling:

src/app/api/robust.ts
// src/app/api/robust.ts → /api/robust
import { Hono } from 'hono'

const app = new Hono()

app.onError((err, c) => {
  const isDev = getEnv(c as any, 'NODE_ENV') === 'development'
  return c.json({
    error: 'Internal Server Error',
    ...(isDev && { details: err.message }),
  }, 500)
})

app.notFound((c) => c.json({ error: 'Not Found' }, 404))

app.get('/robust/users/:id', (c) => {
  const id = c.req.param('id')
  if (id === 'admin') {
    return c.json({ error: 'Access denied' }, 403)
  }
  return c.json({ id, name: 'John' })
})

export default app

Nested Routes

Organize complex APIs with nested sub-routers:

src/app/api/index.ts
// src/app/api/index.ts → /api
import { Hono } from 'hono'

const app = new Hono()

const users = new Hono()
  .get('/users', (c) => c.json({ users: [] }))
  .get('/users/:id', (c) => c.json({ id: c.req.param('id') }))

const posts = new Hono()
  .get('/posts', (c) => c.json({ posts: [] }))
  .get('/posts/:id', (c) => c.json({ id: c.req.param('id') }))

app.route('/', users)
app.route('/', posts)

export default app

When to Use Hono

ScenarioRecommendation
Multiple endpoints in one file Hono
Need middleware (CORS, auth, logging) Hono
Complex routing patterns Hono
Production APIs with many routes Hono
Single endpoint with simple logic Plain handler
Quick prototypes Plain handler