Apidog

All-in-one Collaborative API Development Platform

API Design

API Documentation

API Debugging

API Mocking

API Automated Testing

Getting Started with Hono.js: A Beginner's Guide

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.

Mark Ponomarev

Mark Ponomarev

Updated on May 11, 2025

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 a great API Testing tool that generates beautiful API Documentation?

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!
button

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:

  1. We import the Hono class from the hono package.
  2. We create a new instance of Hono, typically named app.
  3. We define a route for GET requests to the root path (/).
  4. The route handler is a function that takes a Context object (conventionally c) as an argument.
  5. c.text('Hello Hono!') creates a Response object with the plain text "Hello Hono!".
  6. 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 like c.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. The Content-Type is automatically set to application/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 new Response object. body can be null, ReadableStream, ArrayBuffer, FormData, URLSearchParams, or string.
  • 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 must await 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 a great API Testing tool that generates beautiful API Documentation?

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!
button
Top 9 MCP Servers for Git Tools in 2025: Boost Your Development WorkflowViewpoint

Top 9 MCP Servers for Git Tools in 2025: Boost Your Development Workflow

Discover the top 10 MCP servers enhancing Git tools with AI in 2025. Automate workflows, manage repositories, and boost productivity using tools like Apidog.

Ashley Innocent

May 9, 2025

Curror Student Verification Error: Why It Happens and How to Fix It?Viewpoint

Curror Student Verification Error: Why It Happens and How to Fix It?

Struggling with Cursor student verification errors? Learn how to troubleshoot common issues, unlock your free Pro plan, and supercharge your development workflow by integrating Apidog MCP Server with Cursor for intelligent, API-driven coding.

Oliver Kingsley

May 9, 2025

Open Computer Agent: The Open Source Alternative to $200/month OpenAI OperatorViewpoint

Open Computer Agent: The Open Source Alternative to $200/month OpenAI Operator

Discover the Open Computer Agent, a free open-source alternative to OpenAI’s $200/month Operator. Explore its technical architecture, features, and smolagents library.

Ashley Innocent

May 8, 2025