In the fast-evolving world of web development, finding the right framework can be challenging. You want something fast, lightweight, and flexible enough to work across different environments. Enter Hono – a web framework that's rapidly gaining traction among developers for its impressive speed, minimal footprint, and developer-friendly design.
Hono (meaning "flame" 🔥 in Japanese) is aptly named – it's blazingly fast and lights up your development experience with its elegant simplicity. Whether you're building APIs, microservices, or full-stack applications, Hono offers a compelling alternative to heavier frameworks.
This guide will walk you through everything you need to know to get started with Hono.js, from installation to building your first application and understanding its core concepts.
Want an integrated, All-in-One platform for your Developer Team to work together with maximum productivity?
Apidog delivers all your demans, and replaces Postman at a much more affordable price!
What is Hono?

Hono is a small, simple, and ultrafast web framework built on Web Standards. It works on practically any JavaScript runtime: Cloudflare Workers, Fastly Compute, Deno, Bun, Vercel, Netlify, AWS Lambda, Lambda@Edge, and Node.js.
At its core, Hono embraces the Web Standards APIs (like Request, Response, and Fetch), which gives it remarkable portability across platforms. This means you can write your code once and deploy it almost anywhere without major modifications.
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hono!'))
export default app
This simple example demonstrates Hono's clean syntax, inspired by Express but modernized for today's JavaScript ecosystem.
Let's dive in!
1. Install Hono.js with Bun and Project Setup
Bun is a modern JavaScript runtime known for its speed and all-in-one toolkit, including a package manager, bundler, and test runner. Setting up Hono with Bun is straightforward.
Prerequisites
Ensure you have Bun installed. If not, you can install it by following the instructions on the official Bun website.
Creating a New Hono Project with Bun
The quickest way to start a new Hono project with Bun is by using the create-hono
scaffolding tool:
bun create hono@latest my-hono-app
This command will prompt you to choose a template. For this guide, select the bun
template.
? Which template do you want to use? › - Use arrow keys. Return to submit.
❯ bun
cloudflare-workers
cloudflare-pages
deno
fastly
netlify
nodejs
vercel
After selecting the template, navigate into your new project directory and install the dependencies:
cd my-hono-app
bun install
Adding Hono to an Existing Bun Project
If you have an existing Bun project, you can add Hono as a dependency:
bun add hono
2. Building a Basic Application with Hono.js: "Hello Hono!"
Let's create a simple "Hello World" (or rather, "Hello Hono!") application.
Inside your project, you should have a src
directory. Create or open src/index.ts
(or index.js
if you prefer plain JavaScript, though TypeScript is recommended for Hono's full benefits).
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
export default app
Let's break this down:
- We import the
Hono
class from thehono
package. - We create a new instance of
Hono
, typically namedapp
. - We define a route for
GET
requests to the root path (/
). - The route handler is a function that takes a
Context
object (conventionallyc
) as an argument. c.text('Hello Hono!')
creates a Response object with the plain text "Hello Hono!".- Finally, we export the
app
instance. For Bun, this default export is sufficient for the development server to pick it up.
Running Your Application
To run your application in development mode, use the dev
script defined in your package.json
(which create-hono
sets up for you):
bun run dev
This will typically start a server on http://localhost:3000
. Open this URL in your web browser, and you should see "Hello Hono!".
Changing the Port
If you need to run your application on a different port, you can modify the export in src/index.ts
:
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono on port 8080!')
})
export default {
port: 8080, // Specify your desired port
fetch: app.fetch,
}
Now, when you run bun run dev
, the application will be served on http://localhost:8080
.
3. Building a Basic API with Hono.js
Hono excels at building APIs. Let's explore routing, and how to handle requests and craft responses.
Routing
Routing in Hono is intuitive. The app
instance has methods corresponding to HTTP verbs (.get()
, .post()
, .put()
, .delete()
, etc.).
Basic GET Route
We've already seen a basic GET route:
app.get('/hello', (c) => {
return c.text('Hello, world!')
})
Path Parameters
You can define routes with path parameters using the :paramName
syntax. These parameters can be accessed via c.req.param('paramName')
.
// Example: /users/123
app.get('/users/:id', (c) => {
const userId = c.req.param('id')
return c.text(`User ID: ${userId}`)
})
Query Parameters
Query parameters from the URL (e.g., /search?q=hono
) can be accessed using c.req.query('queryName')
.
// Example: /search?category=frameworks&limit=10
app.get('/search', (c) => {
const category = c.req.query('category')
const limit = c.req.query('limit')
return c.text(`Searching in category: ${category}, Limit: ${limit}`)
})
You can also get all query parameters as an object with c.req.query()
.
Handling Different HTTP Methods
Hono makes it easy to handle various HTTP methods for the same path:
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
// GET a list of posts
app.get('/posts', (c) => {
// In a real app, you'd fetch this from a database
const posts = [
{ id: '1', title: 'Getting Started with Hono' },
{ id: '2', title: 'Advanced Hono Techniques' },
];
return c.json(posts); // Return JSON response
})
// GET a single post by ID
app.get('/posts/:id', (c) => {
const id = c.req.param('id')
// Fetch post by ID from a database
const post = { id: id, title: `Post ${id}`, content: 'This is the content...' };
if (!post) {
return c.notFound() // Built-in helper for 404
}
return c.json(post)
})
// CREATE a new post
app.post('/posts', async (c) => {
const body = await c.req.json() // Parse JSON body
// In a real app, you'd save this to a database
console.log('Creating post:', body)
return c.json({ message: 'Post created successfully!', data: body }, 201) // Respond with 201 Created
})
// UPDATE an existing post
app.put('/posts/:id', async (c) => {
const id = c.req.param('id')
const body = await c.req.json()
// Update post in database
console.log(`Updating post ${id}:`, body)
return c.json({ message: `Post ${id} updated successfully!`, data: body })
})
// DELETE a post
app.delete('/posts/:id', (c) => {
const id = c.req.param('id')
// Delete post from database
console.log(`Deleting post ${id}`)
return c.json({ message: `Post ${id} deleted successfully!` })
})
export default {
port: 3000,
fetch: app.fetch
}
Request Object (c.req
)
The Context
object (c
) provides access to the request details via c.req
. Key properties and methods include:
c.req.url
: The full URL string.c.req.method
: The HTTP method (e.g., 'GET', 'POST').c.req.headers
: A Headers object. Access headers likec.req.header('Content-Type')
.c.req.param('name')
: Get a path parameter.c.req.query('name')
: Get a query parameter.c.req.queries('name')
: Get all values for a repeated query parameter as an array.c.req.json()
: Parses the request body as JSON. (Returns a Promise)c.req.text()
: Reads the request body as plain text. (Returns a Promise)c.req.arrayBuffer()
: Reads the request body as an ArrayBuffer. (Returns a Promise)c.req.formData()
: Parses the request body as FormData. (Returns a Promise)c.req.valid('type')
: Access validated data (used with validators, covered later).
Response Object (c
) and Helpers
The Context
object (c
) also provides convenient helpers for creating Response
objects:
c.text(text, status?, headers?)
: Returns a plain text response.c.json(object, status?, headers?)
: Returns a JSON response. TheContent-Type
is automatically set toapplication/json
.c.html(html, status?, headers?)
: Returns an HTML response.c.redirect(location, status?)
: Returns a redirect response (default status 302).c.notFound()
: Returns a 404 Not Found response.c.newResponse(body, status?, headers?)
: Creates a newResponse
object.body
can benull
,ReadableStream
,ArrayBuffer
,FormData
,URLSearchParams
, orstring
.c.header(name, value)
: Sets a response header.
Example: Setting Custom Headers
app.get('/custom-header', (c) => {
c.header('X-Powered-By', 'HonoJS-Flame')
c.header('Cache-Control', 'no-cache')
return c.text('Check the response headers!')
})
4. Working with Middleware in Hono.js
Middleware functions are a cornerstone of Hono (and many web frameworks). They are functions that can process a request before it reaches the main route handler, or process a response after the handler has run. Middleware is great for tasks like logging, authentication, data validation, compression, CORS, and more.
The basic signature of a middleware function is async (c, next) => { ... }
.
c
: The Context object.next
: A function to call to pass control to the next middleware in the chain, or to the final route handler. You mustawait next()
if you want subsequent middleware or the handler to execute.
Using Built-in Middleware
Hono comes with a rich set of built-in middleware. You can import them from hono/middleware-name
(e.g., hono/logger
, hono/cors
).
To apply middleware to all routes, use app.use(middlewareFunction)
.
To apply middleware to specific routes, provide a path pattern as the first argument: app.use('/admin/*', authMiddleware)
.
Example: Logger and ETag Middleware
// src/index.ts
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { etag } from 'hono/etag'
import { prettyJSON } from 'hono/pretty-json' // For nicely formatted JSON responses
const app = new Hono()
// Apply middleware to all routes
app.use(logger()) // Logs request and response info to the console
app.use(etag()) // Adds ETag headers for caching
app.use(prettyJSON()) // Formats JSON responses with indentation
app.get('/', (c) => {
return c.text('Hello with Logger and ETag!')
})
app.get('/data', (c) => {
return c.json({ message: 'This is some data.', timestamp: Date.now() })
})
export default app
When you run this and access /
or /data
, you'll see logs in your console, and responses will include an ETag
header. JSON responses from /data
will be nicely formatted.
Other Useful Built-in Middleware:
cors
: Handles Cross-Origin Resource Sharing.basicAuth
: Implements Basic Authentication.jwt
: JWT (JSON Web Token) authentication.compress
: Compresses response bodies.cache
: Implements caching using the Cache API.secureHeaders
: Adds various security-related HTTP headers.bodyLimit
: Limits the size of the request body.
You can find a full list and documentation in the Hono docs under "Middleware".
Creating Custom Middleware
You can easily write your own middleware.
Example 1: Simple Request Timer
// src/index.ts
import { Hono } from 'hono'
import { logger } from 'hono/logger'
const app = new Hono()
app.use(logger())
// Custom middleware to measure request processing time
app.use(async (c, next) => {
const start = Date.now()
console.log(`Request received at: ${new Date(start).toISOString()}`)
await next() // Call the next middleware or route handler
const ms = Date.now() - start
c.header('X-Response-Time', `${ms}ms`) // Add custom header to the response
console.log(`Request processed in ${ms}ms`)
})
app.get('/', (c) => {
return c.text('Hello from Hono with custom timing middleware!')
})
app.get('/slow', async (c) => {
// Simulate a slow operation
await new Promise(resolve => setTimeout(resolve, 1500))
return c.text('This was a slow response.')
})
export default app
Access /
and /slow
to see the timing logs and the X-Response-Time
header.
Example 2: Simple API Key Authentication Middleware
This is a very basic example for demonstration. In a real application, use more robust authentication methods like JWT or OAuth, and store API keys securely.
// src/index.ts
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { HTTPException } from 'hono/http-exception' // For throwing standard HTTP errors
const app = new Hono()
app.use(logger())
const API_KEY = "supersecretapikey"; // In a real app, store this securely (e.g., env variable)
// Custom API Key Authentication Middleware
const apiKeyAuth = async (c, next) => {
const apiKeyHeader = c.req.header('X-API-KEY')
if (apiKeyHeader && apiKeyHeader === API_KEY) {
await next()
} else {
// Using HTTPException to return a standardized error response
throw new HTTPException(401, { message: 'Unauthorized: Invalid API Key' })
}
}
// Apply this middleware to a specific group of routes
app.use('/api/v1/*', apiKeyAuth)
app.get('/api/v1/me', (c) => {
return c.json({ user: 'Authenticated User', email: 'user@example.com' })
})
app.get('/public/info', (c) => {
return c.text('This is public information, no API key needed.')
})
// Error handler for HTTPException
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse()
}
// For other errors
console.error('Unhandled error:', err)
return c.text('Internal Server Error', 500)
})
export default app
To test this:
curl http://localhost:3000/api/v1/me
(should return 401 Unauthorized)curl -H "X-API-KEY: supersecretapikey" http://localhost:3000/api/v1/me
(should return user data)curl http://localhost:3000/public/info
(should return public info)
Reusing Custom Middleware with createMiddleware
For more complex or reusable middleware, you can define it separately using createMiddleware
from hono/factory
. This also helps with TypeScript type inference.
// src/middlewares/timing.ts
import { createMiddleware } from 'hono/factory'
export const timingMiddleware = createMiddleware(async (c, next) => {
const start = Date.now()
await next()
const ms = Date.now() - start
c.res.headers.set('X-Response-Time', `${ms}ms`) // Note: c.res.headers.set
console.log(` -> Processed in ${ms}ms`)
})
// src/middlewares/auth.ts
import { createMiddleware } from 'hono/factory'
import { HTTPException } from 'hono/http-exception'
const VALID_API_KEY = "anothersecretkey";
// Type for environment variables if your middleware needs them
type Env = {
Variables: {
user?: { id: string } // Example: set user data after auth
}
// Bindings: { MY_KV_NAMESPACE: KVNamespace } // Example for Cloudflare Workers
}
export const secureApiKeyAuth = createMiddleware<Env>(async (c, next) => {
const apiKey = c.req.header('Authorization')?.replace('Bearer ', '')
if (apiKey === VALID_API_KEY) {
// Optionally, you can set variables in the context for downstream handlers
c.set('user', { id: 'user123' })
await next()
} else {
throw new HTTPException(401, { message: 'Access Denied: Secure API Key Required.'})
}
})
// src/index.ts
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { timingMiddleware } from './middlewares/timing' // Assuming they are in a middlewares folder
import { secureApiKeyAuth } from './middlewares/auth'
const app = new Hono()
app.use(logger())
app.use(timingMiddleware) // Apply to all routes
app.get('/', (c) => {
return c.text('Hello from Hono with factored middleware!')
})
app.use('/secure/data/*', secureApiKeyAuth) // Apply only to /secure/data/*
app.get('/secure/data/profile', (c) => {
// const user = c.get('user') // Access variable set by middleware
return c.json({ profileData: 'Sensitive profile information', /*user: user*/ })
})
app.get('/public/data', (c) => {
return c.text('This is public data, no key needed.')
})
// General error handler
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse();
}
console.error('Error:', err.message);
return c.json({ error: 'Internal Server Error', message: err.message }, 500);
});
export default app
This structure makes your middleware more modular and easier to test independently.
5. Testing Your Hono.js Applications
Hono is designed to be easily testable. Bun comes with its own test runner, bun:test
, which works well with Hono. You can also use other test runners like Vitest.
Using bun:test
(Basic)
The bun.md
documentation provides a basic example of testing with bun:test
:
Create a test file, for example, src/index.test.ts
:
// src/index.test.ts
import { describe, expect, it } from 'bun:test'
import app from '.' // Imports your app from src/index.ts
describe('Hono Application Tests', () => {
it('Should return 200 OK for GET /', async () => {
const req = new Request('http://localhost/') // Base URL doesn't matter much here
const res = await app.fetch(req)
expect(res.status).toBe(200)
expect(await res.text()).toBe('Hello Hono!') // Or whatever your root route returns
})
it('Should return JSON for GET /posts', async () => {
const req = new Request('http://localhost/posts')
const res = await app.fetch(req)
expect(res.status).toBe(200)
const json = await res.json()
expect(json).toBeArray() // Assuming /posts returns an array
// Add more specific assertions about the JSON content if needed
})
it('Should create a post for POST /posts', async () => {
const postData = { title: 'Test Post', content: 'This is a test.' };
const req = new Request('http://localhost/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postData),
});
const res = await app.fetch(req);
expect(res.status).toBe(201); // Expect 201 Created
const jsonResponse = await res.json();
expect(jsonResponse.message).toBe('Post created successfully!');
expect(jsonResponse.data.title).toBe(postData.title);
});
})
To run your tests:
bun test
Or, if you want to run a specific file:
bun test src/index.test.ts
Using app.request()
Helper
Hono provides a convenient app.request()
method which simplifies testing by allowing you to directly pass a path and options, rather than constructing a full Request
object every time. This is shown in docs/guides/testing.md
.
// src/index.test.ts
import { describe, expect, it } from 'bun:test' // Or import from 'vitest'
import app from '.' // Your Hono app instance
describe('Hono App with app.request()', () => {
it('GET / should return "Hello Hono!"', async () => {
const res = await app.request('/')
expect(res.status).toBe(200)
expect(await res.text()).toBe('Hello Hono!') // Adjust to your actual root response
})
it('GET /posts/:id should return a specific post', async () => {
// Assuming your app has a route like app.get('/posts/:id', ...)
// and for this test, it might return { id: '123', title: 'Test Post 123' }
const res = await app.request('/posts/123')
expect(res.status).toBe(200)
const data = await res.json()
expect(data.id).toBe('123')
// expect(data.title).toBe('Post 123') // Or based on your app's logic
})
it('POST /posts should create a new post', async () => {
const newPost = { title: 'My New Post', content: 'Awesome content here.' }
const res = await app.request('/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newPost),
})
expect(res.status).toBe(201) // Assuming your POST /posts returns 201
const responseData = await res.json()
expect(responseData.message).toBe('Post created successfully!')
expect(responseData.data.title).toBe(newPost.title)
})
it('GET /api/v1/me without API key should be 401', async () => {
// Assuming the API key middleware from earlier example is active on this route
const res = await app.request('/api/v1/me')
expect(res.status).toBe(401)
})
it('GET /api/v1/me with correct API key should be 200', async () => {
const res = await app.request('/api/v1/me', {
headers: {
'X-API-KEY': 'supersecretapikey' // Use the key from your middleware
}
})
expect(res.status).toBe(200)
const data = await res.json();
expect(data.user).toBe('Authenticated User')
})
})
Using testClient()
Helper for Type-Safe Testing
For even better type safety, especially if you're using Hono's RPC capabilities or have well-defined route schemas, Hono provides a testClient
helper (from hono/testing
). This client is typed based on your Hono application's routes, giving you autocompletion and type checking in your tests.
Important Note for testClient
Type Inference:
For testClient
to correctly infer types, you must define your routes using chained methods directly on the Hono
instance (e.g., const app = new Hono().get(...).post(...)
) or export the route type if using RPC. If you define routes separately (e.g., const app = new Hono(); app.get(...)
), type inference for testClient
might be limited.
// src/app-for-test-client.ts
// To demonstrate testClient, let's define an app with chained routes
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator' // Example for typed query params
import { z } from 'zod'
const appWithChainedRoutes = new Hono()
.get('/search',
zValidator('query', z.object({ q: z.string(), limit: z.coerce.number().optional() })),
(c) => {
const { q, limit } = c.req.valid('query')
return c.json({ query: q, limit: limit || 10, results: [`result for ${q} 1`, `result for ${q} 2`] })
}
)
.post('/submit',
zValidator('json', z.object({ name: z.string(), email: z.string().email() })),
async (c) => {
const data = c.req.valid('json')
return c.json({ message: `Received data for ${data.name}`, data }, 201)
}
);
export default appWithChainedRoutes;
// src/app-for-test-client.test.ts
import { describe, expect, it } from 'bun:test'
import { testClient } from 'hono/testing'
import appWithChainedRoutes from './app-for-test-client' // Import the app
describe('Hono App with testClient', () => {
const client = testClient(appWithChainedRoutes)
it('GET /search should return typed results', async () => {
const res = await client.search.$get({
query: { q: 'hono rocks' }
})
expect(res.status).toBe(200)
const data = await res.json()
expect(data.query).toBe('hono rocks')
expect(data.results.length).toBeGreaterThan(0)
})
it('GET /search with limit should respect it', async () => {
const res = await client.search.$get({
query: { q: 'hono with limit', limit: '5' } // Query params are strings initially
})
expect(res.status).toBe(200)
const data = await res.json()
expect(data.limit).toBe(5) // Zod validator coerced it to number
})
it('POST /submit should accept valid data', async () => {
const payload = { name: 'Test User', email: 'test@example.com' }
// For POST, PUT, etc. with JSON body, it's client.path.$post({ json: payload })
const res = await client.submit.$post({
json: payload
})
expect(res.status).toBe(201)
const data = await res.json()
expect(data.message).toBe(`Received data for ${payload.name}`)
expect(data.data.email).toBe(payload.email)
});
it('POST /submit with headers', async () => {
const payload = { name: 'Test User Headers', email: 'testheaders@example.com' }
const res = await client.submit.$post({
json: payload
}, {
headers: {
'X-Custom-Test-Header': 'Testing123'
}
})
expect(res.status).toBe(201)
// You'd typically have a route/middleware that reads this header to verify
});
})
To run this test, assuming you have @hono/zod-validator
and zod
installed (bun add @hono/zod-validator zod
):
bun test src/app-for-test-client.test.ts
This testClient
provides a much nicer developer experience for testing, especially for APIs with defined schemas.
Conclusion
Hono offers a fresh approach to web development with its combination of speed, lightweight design, and developer-friendly features. This guide has covered the basics to get you started, but there's much more to explore:
- Advanced routing techniques
- More complex middleware patterns
- Server-side rendering with JSX
- Integration with databases
- RPC-style APIs with type safety using Hono Client
- Deployment to various platforms
As you continue your journey with Hono, refer to the official documentation and examples repository for more in-depth information and inspiration.
The web development landscape is constantly evolving, and Hono represents a modern approach that embraces Web Standards, prioritizes performance, and works across the growing ecosystem of JavaScript runtimes. Whether you're building simple APIs or complex applications, Hono provides the tools you need without unnecessary overhead.
Start small, experiment, and watch your applications grow with Hono – the lightweight, ultrafast web framework that lives up to its fiery name. 🔥
Want an integrated, All-in-One platform for your Developer Team to work together with maximum productivity?
Apidog delivers all your demans, and replaces Postman at a much more affordable price!