* Update README.md Adding some clarity on how to access bindings like secrets * Update README.md
17 KiB
Hono - [炎] means flame🔥 in Japanese - is a small, simple, and ultrafast web framework for Cloudflare Workers or Service Worker based serverless such as Fastly Compute@Edge.
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hono!!'))
app.fire()
Features
- Ultrafast - the router does not use linear loops.
- Zero-dependencies - using only Service Worker and Web Standard API.
- Middleware - built-in middleware and ability to extend with your own middleware.
- TypeScript - first-class TypeScript support.
- Optimized - for Cloudflare Workers.
Benchmark
Hono is fastest, compared to other routers for Cloudflare Workers.
hono - trie-router(default) x 389,510 ops/sec ±3.16% (85 runs sampled)
hono - regexp-router x 452,290 ops/sec ±2.64% (84 runs sampled)
itty-router x 206,013 ops/sec ±3.39% (90 runs sampled)
sunder x 323,131 ops/sec ±0.75% (97 runs sampled)
worktop x 191,218 ops/sec ±2.70% (91 runs sampled)
Fastest is hono - regexp-router
✨ Done in 43.56s.
Why so fast?
Routers used in Hono are really smart.
- TrieRouter(default) - Implemented with Trie tree structure.
- RegExpRouter - Match the route with using one big Regex made before dispatch.
Hono in 1 minute
A demonstration to create an application for Cloudflare Workers with Hono.
Not only fast
Hono is fast. But not only fast.
Write Less, do more
Built-in middleware make "Write Less, do more" in reality. You can use a lot of middleware without writing code from scratch. Below are examples.
- Basic Authentication
- Cookie parsing / serializing
- CORS
- ETag
- GraphQL Server
- JWT Authentication
- Logger
- Mustache template engine (Only for Cloudflare Workers)
- JSON pretty printing
- Serving static files (Only for Cloudflare Workers)
To enable logger and Etag middleware with just this code.
import { Hono } from 'hono'
import { etag } from 'hono/etag'
import { logger } from 'hono/logger'
const app = new Hono()
app.use('*', etag(), logger())
And, the routing of Hono is so flexible. It's easy to construct large web applications.
import { Hono } from 'hono'
import { basicAuth } from 'hono/basic-auth'
const v1 = new Hono()
v1.get('/posts', (c) => {
return c.text('list posts')
})
.post(basicAuth({ username, password }), (c) => {
return c.text('created!', 201)
})
.get('/posts/:id', (c) => {
const id = c.req.param('id')
return c.text(`your id is ${id}`)
})
const app = new Hono()
app.route('/v1', v1)
Web Standard
Request and Response object used in Hono are extensions of the Web Standard Fetch API. If you are familiar with that, you don't need to know more than that.
Developer Experience
Hono provides fine "Developer Experience". Easy access to Request/Response thanks to the Context
object.
Above all, Hono is written in TypeScript. So, Hono has "Types"!
For example, the named path parameters will be literal types.
Install
You can install Hono from the npm registry.
npm install hono
Methods
An instance of Hono
has these methods.
- app.HTTP_METHOD([path,]handler|middleware...)
- app.all([path,]handler|middleware...)
- app.route(path, [app])
- app.use([path,]middleware)
- app.notFound(handler)
- app.onError(err, handler)
- app.fire()
- app.fetch(request, env, event)
- app.request(path, options)
Routing
Basic
// HTTP Methods
app.get('/', (c) => c.text('GET /'))
app.post('/', (c) => c.text('POST /'))
app.put('/', (c) => c.text('PUT /'))
app.delete('/', (c) => c.text('DELETE /'))
// Wildcard
app.get('/wild/*/card', (c) => {
return c.text('GET /wild/*/card')
})
// Any HTTP methods
app.all('/hello', (c) => c.text('Any Method /hello'))
Named Parameter
app.get('/user/:name', (c) => {
const name = c.req.param('name')
...
})
or all parameters at once:
app.get('/posts/:id/comment/:comment_id', (c) => {
const { id, comment_id } = c.req.param()
...
})
Regexp
app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => {
const { date, title } = c.req.param()
...
})
Chained route
app
.get('/endpoint', (c) => {
return c.text('GET /endpoint')
})
.post((c) => {
return c.text('POST /endpoint')
})
.delete((c) => {
return c.text('DELETE /endpoint')
})
no strict
If strict
is set false, /hello
and/hello/
are treated the same.
const app = new Hono({ strict: false }) // Default is true
app.get('/hello', (c) => c.text('/hello or /hello/'))
async/await
app.get('/fetch-url', async (c) => {
const response = await fetch('https://example.com/')
return c.text(`Status is ${response.status}`)
})
Grouping
Group the routes with Hono
instance and add them to the main app with route
method.
const book = new Hono()
book.get('/', (c) => c.text('List Books')) // GET /book
book.get('/:id', (c) => {
// GET /book/:id
const id = c.req.param('id')
return c.text('Get Book: ' + id)
})
book.post('/', (c) => c.text('Create Book')) // POST /book
const app = new Hono()
app.route('/book', book)
Middleware
Middleware works after/before Handler. We can get Request
before dispatching or manipulate Response
after dispatching.
Definition of Middleware
- Handler - should return
Response
object. Only one handler will be called. - Middleware - should return nothing, will be proceeded to next middleware with
await next()
The user can register middleware using c.use
or using c.HTTP_METHOD
as well as the handlers. For this feature, it's easy to specify the path and the method.
// match any method, all routes
app.use('*', logger())
// specify path
app.use('/posts/*', cors())
// specify method and path
app.post('/posts/*', basicAuth(), bodyParse())
If the handler returns Response
, it will be used for the end-user, and stopping the processing.
app.post('/posts', (c) => c.text('Created!', 201))
In this case, four middleware are processed before dispatching like this:
logger() -> cors() -> basicAuth() -> bodyParse() -> *handler*
Built-in Middleware
Hono has built-in middleware.
import { Hono } from 'hono'
import { poweredBy } from 'hono/powered-by'
import { logger } from 'hono/logger'
import { basicAuth } from 'hono/basicAuth'
const app = new Hono()
app.use('*', poweredBy())
app.use('*', logger())
app.use(
'/auth/*',
basicAuth({
username: 'hono',
password: 'acoolproject',
})
)
Available built-in middleware is listed on src/middleware.
Custom Middleware
You can write your own middleware.
// Custom logger
app.use('*', async (c, next) => {
console.log(`[${c.req.method}] ${c.req.url}`)
await next()
})
// Add a custom header
app.use('/message/*', async (c, next) => {
await next()
c.header('x-message', 'This is middleware!')
})
app.get('/message/hello', (c) => c.text('Hello Middleware!'))
Not Found
app.notFound
for customizing Not Found Response.
app.notFound((c) => {
return c.text('Custom 404 Message', 404)
})
Error Handling
app.onError
handle the error and return the customized Response.
app.onError((err, c) => {
console.error(`${err}`)
return c.text('Custom Error Message', 500)
})
Context
To handle Request and Response, you can use Context
object.
c.req
// Get Request object
app.get('/hello', (c) => {
const userAgent = c.req.headers.get('User-Agent')
...
})
// Shortcut to get a header value
app.get('/shortcut', (c) => {
const userAgent = c.req.header('User-Agent')
...
})
// Query params
app.get('/search', (c) => {
const query = c.req.query('q')
...
})
// Get all params at once
app.get('/search', (c) => {
const { q, limit, offset } = c.req.query()
...
})
// Multiple query values
app.get('/search', (c) => {
const queries = c.req.queries('q')
// ---> GET search?q=foo&q=bar
// queries[0] => foo, queries[1] => bar
...
})
// Captured params
app.get('/entry/:id', (c) => {
const id = c.req.param('id')
...
})
Shortcuts for Response
app.get('/welcome', (c) => {
// Set headers
c.header('X-Message', 'Hello!')
c.header('Content-Type', 'text/plain')
// Set HTTP status code
c.status(201)
// Return the response body
return c.body('Thank you for comming')
})
The Response is the same as below.
new Response('Thank you for comming', {
status: 201,
headers: {
'X-Message': 'Hello',
'Content-Type': 'text/plain',
},
})
c.text()
Render text as Content-Type:text/plain
.
app.get('/say', (c) => {
return c.text('Hello!')
})
c.json()
Render JSON as Content-Type:application/json
.
app.get('/api', (c) => {
return c.json({ message: 'Hello!' })
})
c.html()
Render HTML as Content-Type:text/html
.
app.get('/', (c) => {
return c.html('<h1>Hello! Hono!</h1>')
})
c.notFound()
Return the Not Found
Response.
app.get('/notfound', (c) => {
return c.notFound()
})
c.redirect()
Redirect, default status code is 302
.
app.get('/redirect', (c) => c.redirect('/'))
app.get('/redirect-permanently', (c) => c.redirect('/', 301))
c.res
// Response object
app.use('/', async (c, next) => {
await next()
c.res.headers.append('X-Debug', 'Debug message')
})
c.event
// FetchEvent object
app.get('/foo', async (c) => {
c.event.waitUntil(
c.env.KV.put(key, data)
)
...
})
c.env
Environment variables, secrets, and KV namespaces are known as bindings. Regardless of type, bindings are always available as global variables and can be accessed via the context c.env.BINDING_KEY
.
// Environment object for Cloudflare Workers
app.get('*', async c => {
const counter = c.env.COUNTER
...
})
fire
app.fire()
do this.
addEventListener('fetch', (event) => {
event.respondWith(this.handleEvent(event))
})
fetch
app.fetch
for Cloudflare Module Worker syntax.
export default {
fetch(request: Request, env: Env, event: FetchEvent) {
return app.fetch(request, env, event)
},
}
or just do:
export default app
request
request
is a useful method for testing.
test('GET /hello is ok', async () => {
const res = await app.request('http://localhost/hello')
expect(res.status).toBe(200)
})
router
The router
option specify which router is used inside. The default router is TrieRouter
. If you want to use RexExpRouter
, write like this:
import { RegExpRouter } from 'hono/router/reg-exp-router'
const app = new Hono({ router: new RegExpRouter() })
Routing Ordering
The routing priority is decided by the order of registration. Only one handler will be dispatched.
app.get('/book/a', (c) => c.text('a')) // a
app.get('/book/:slug', (c) => c.text('common')) // common
GET /book/a ---> `a` // common will not be dispatched
GET /book/b ---> `common` // a will not be dispatched
All scoring rules:
app.get('/api/*', 'c') // score 1.1 <--- `/*` is special wildcard
app.get('/api/:type/:id', 'd') // score 3.2
app.get('/api/posts/:id', 'e') // score 3.3
app.get('/api/posts/123', 'f') // score 3.4
app.get('/*/*/:id', 'g') // score 3.5
app.get('/api/posts/*/comment', 'h') // score 4.6 - not match
app.get('*', 'a') // score 0.7
app.get('*', 'b') // score 0.8
GET /api/posts/123
---> will match => c, d, e, f, b, a, b
---> sort by score => a, b, c, d, e, f, g
Cloudflare Workers with Hono
Using Wrangler, you can develop the application locally and publish it with few commands.
Let's write your first code for Cloudflare Workers with Hono.
1. wrangler init
Initialize as a wrangler project.
mkdir hono-example
cd hono-example
npx wrangler init -y
2. npm install hono
Install hono
from the npm registry.
npm init -y
npm i hono
3. Write your app
Edit src/index.ts
. Only 4 lines!!
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello! Hono!'))
app.fire()
4. Run
Run the development server locally. Then, access http://127.0.0.1:8787/
in your Web browser.
npx wrangler dev
5. Publish
Deploy to Cloudflare. That's all!
npx wrangler publish ./src/index.ts
Starter template
You can start making your Cloudflare Workers application with the starter template. It is really minimal using TypeScript, esbuild, Miniflare, and Jest.
To generate a project skeleton, run this command.
npx create-cloudflare my-app https://github.com/honojs/hono-minimal
Practical Example
How about writing web API with Hono?
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { basicAuth } from 'hono/basic-auth'
import { prettyJSON } from 'hono/pretty-json'
import { getPosts, getPost, createPost, Post } from './model'
const app = new Hono()
app.get('/', (c) => c.text('Pretty Blog API'))
app.use('*', prettyJSON())
app.notFound((c) => c.json({ message: 'Not Found', ok: false }, 404))
export interface Bindings {
USERNAME: string
PASSWORD: string
}
const api = new Hono<Bindings>()
api.use('/posts/*', cors())
api.get('/posts', (c) => {
const { limit, offset } = c.req.query()
const posts = getPosts({ limit, offset })
return c.json({ posts })
})
api.get('/posts/:id', (c) => {
const id = c.req.param('id')
const post = getPost({ id })
return c.json({ post })
})
api.post(
'/posts',
async (c, next) => {
const auth = basicAuth({ username: c.env.USERNAME, password: c.env.PASSWORD })
await auth(c, next)
},
async (c) => {
const post = await c.req.json<Post>()
const ok = createPost({ post })
return c.json({ ok })
}
)
app.route('/api', api)
export default app
Other Examples
- Hono Examples - https://github.com/honojs/examples
Related projects
Implementation of the original router TrieRouter
is inspired by goblin. RegExpRouter
is inspired by Router::Boom. API design is inspired by express and koa. itty-router, Sunder, and worktop are the other routers or frameworks for Cloudflare Workers.
- express - https://github.com/expressjs/express
- koa - https://github.com/koajs/koa
- itty-router - https://github.com/kwhitley/itty-router
- Sunder - https://github.com/SunderJS/sunder
- goblin - https://github.com/bmf-san/goblin
- worktop - https://github.com/lukeed/worktop
- Router::Boom - https://github.com/tokuhirom/Router-Boom
Contributing
Contributions Welcome! You can contribute in the following ways.
- Write or fix documents
- Write code of middleware
- Fix bugs
- Refactor the code
- etc.
Contributors
Thanks to all contributors! Especially, @metrue and @usualoma!
Author
Yusuke Wada https://github.com/yusukebe
License
Distributed under the MIT License. See LICENSE for more information.