0
0
mirror of https://github.com/honojs/hono.git synced 2024-11-25 13:19:30 +01:00
Web Framework built on Web Standards https://hono.dev/
Go to file
2022-05-22 08:05:42 +09:00
.github/workflows ci: ci any PR (#197) 2022-05-05 09:35:44 +09:00
.vscode refactor: automate import sorting (#157) 2022-04-20 14:17:55 +09:00
benchmarks chore: fixed benchmark 2022-05-13 08:55:13 +09:00
docs docs: remove Japanese README (#225) 2022-05-13 12:55:55 +09:00
src feat: multiple query values c.req.queries (#260) 2022-05-22 07:54:00 +09:00
.eslintignore Setup lint to enable code styles check (#27) 2022-01-07 07:03:54 +09:00
.eslintrc.js refactor: automate import sorting (#157) 2022-04-20 14:17:55 +09:00
.gitignore chore: ignore sandbox directory 2022-05-13 08:48:04 +09:00
.npmignore refactor: refine directory structure (#156) 2022-04-20 14:06:04 +09:00
.prettierrc docs: create Japanese readme (#131) 2022-03-09 16:03:58 +09:00
jest.config.js chore: absolute import to relative import (#228) 2022-05-14 12:55:01 +09:00
LICENSE Create LICENSE (#1) 2021-12-15 05:06:31 +09:00
package.json chore: bump up to v1.3.6 2022-05-22 08:05:42 +09:00
README.md feat: multiple query values c.req.queries (#260) 2022-05-22 07:54:00 +09:00
tsconfig.build.esm.json feat: serve-static middleware supports Module Worker mode (#250) 2022-05-19 09:29:09 +09:00
tsconfig.build.json feat: serve-static middleware supports Module Worker mode (#250) 2022-05-19 09:29:09 +09:00
tsconfig.json feat: serve-static middleware supports Module Worker mode (#250) 2022-05-19 09:29:09 +09:00
yarn.lock chore: absolute import to relative import (#228) 2022-05-14 12:55:01 +09:00


GitHub Workflow Status GitHub npm npm npm type definitions GitHub commit activity GitHub last commit

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 737,602 ops/sec ±3.65% (67 runs sampled)
hono - regexp-router x 1,188,203 ops/sec ±6.42% (60 runs sampled)
itty-router x 163,970 ops/sec ±3.05% (91 runs sampled)
sunder x 344,468 ops/sec ±0.87% (97 runs sampled)
worktop x 222,044 ops/sec ±2.13% (85 runs sampled)
Fastest is hono - regexp-router
✨  Done in 84.04s.

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.

Demo

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.

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 pots')
})
  .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.

Demo

Install

You can install Hono from the npm registry.

npm install hono

Methods

An instance of Hono has these methods.

  • app.HTTP_METHOD([パス,]handler|middleware...)
  • app.all([パス,]handler|middleware...)
  • app.route(パス, [app])
  • app.use([パス,]middleware)
  • app.notFound(handler)
  • app.onError(err, handler)
  • app.fire()
  • app.fetch(request, env, event)
  • app.request(パス, 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, /helloand/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 operate after/before executing Handler. We can get Response before dispatching or manipulate Response after dispatching.

Definition of Middleware

  • Handler - should return Response object.
  • Middleware - should return nothing, do await next()

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,
  statusText: 'Created',
  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 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)
})

routerClass

The routerClass 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({ routerClass: RegExpRouter })

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 skelton, 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.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.use('/posts/*', cors())

app.route('/api', api)

export default app

Other Examples

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.

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.