❤️‍🔥

Honoの7つのコンセプト

に公開

Hono概要

HonoはバックエンドのためのWebフレームワークです。Cloudflare Workers、Deno、Bun、Node.jsなどどんなJavaScriptのランタイムでも動作します。2021年の12月15日から開発が始まり4年が経とうとしています。

Logo

以下はHonoを使った基本的な"Hello World"のアプリケーションです。Honoのインスタンスを作成し、ルートを定義するだけです。とてもクリーンで可読性に優れています。このシンプルなデザインが受け入れられています。

import { Hono } from 'hono'
const app = new Hono()

app.get('/', (c) => c.text('Hono!'))

export default app

Honoはどんどん人気になっています。現時点でGitHubスターは27K、npmのダウンロード数は月間1,300万です。

Stars

またユーザーも増えています。趣味のユースケースのみならず、多くの企業がプロダクションで使っています。

Users

一番のヘビーユーザーはCloudflareだと思います。D1、KV、R2、Queues、Workers Logsなどのプロダクトの内部で使われています。

https://blog.cloudflare.com/the-story-of-web-framework-hono-from-the-creator-of-hono/

コミュニティも成長しています。今年の10月には第2回Hono Conferenceを行い180人以上の参加者を集めました。

Hono Conf

7つのコンセプト

みなさんはHonoを使ったことをありますか?もしHonoを使ったことがあるとして、「使い方」は知っているでしょう。しかし、その裏に隠されたデザインの思想、言い換えれば「コンセプト」をよく知らないかもしれません。そこで、今回は「Honoの7つのコンセプト」と題し、そのコンセプトを作者の僕が紹介したいと思います。

1. Web標準

Web標準とは「Webに関する標準的な仕様やAPI」のことです。Webを開発するために必要なJavaScript、HTML、CSSなどの技術も含まれます。ここではJavaScriptの話をします。

JavaScriptのWeb標準は当初ブラウザで使うことが前提にされました。しかし、昨今はそのAPIをサーバーサイドで使うことができる環境が出てきました。例えば、Cloudflare Workers、Deno、Bunなどです。またNode.jsでも使えるようになりました。

Web標準のAPIのオブジェクトにはこんなものがあります。

  • Request
  • Response
  • Headers
  • URL
  • URLSearchParams
  • FormData

などです。例えば、Cloudflare Workers、Deno、Bunでは以下のコードが動きます。

export default {
  fetch: (req: Request) => {
    return new Response(`Hello! You accessed ${req.url}`)
  }
}

これはreqという変数にRequestオブジェクトが入っています。Responseのインスタンスを返すことでHTTPのレスポンスを返すわけですが、引数にテキストを渡すとボディがそれになります。req.urlでリクエストのURLが入ります。

Web標準のAPIは「サーバー専用」ではないものの、基本的には「洗練されている」と言えます。またブラウザでの使用実績があるので、品質が保証されています。

Good things

HonoはそのWeb標準のみを使って作られています。例えばいくつかHonoで使っているオブジェクトはWeb標準のオブジェクトをラップしていますが、直接使うこともできます。

import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  const rawRequest = c.req.raw // rawRequest is instance of Request
  return new Response(`Hello! You accessed ${rawRequest.url}`)
})

export default app

ですので、Web標準のAPIに馴染みがあれば、使いやすいフレームワークと言えます。

また、Web標準のAPIのみを使っているので、外部ライブラリへの依存がゼロなのも特徴です。その証拠にhonoパッケージのpackage.jsonにはdependeciesの項目がありません。

package.json

ですので、インストールが非常に短い時間で終わります。依存がないとはいえ、後述するミドルウェアやヘルパーのおかげでhonoパッケージ単体で多くの機能をもったWebアプリケーションを構築することができます。

そして当然ですが、Web標準を使っているということは、Cloudflare Workers、Deno、Bun、Node.js等で動きます。異なるランタイムですが使っているAPIは同じなので挙動は同じです。プラットフォームごとに特徴的な機能はありますが、ロジックの挙動は同じです。これがHonoのアドバンテージで、例えば、Cloudflare Workers用につくっていたアプリをNode.jsで動かす、なんてことも容易です。

Runtimes

2. 軽量

Honoは軽量です。つまりファイルサイズが小さい。一番小さいHonoのアプリケーションはバンドルしてminifyすると11KBです。一方、Node.js専用のフレームワーク「Express」は同じ条件で790KBでした。Honoはとても小さいのです。

Bundle size

小さい秘密のひとつは多くの機能をWeb標準のAPIを活用してるからです。

また、基本的な機能を提供するコアを小さくして後述するミドルウェアやヘルパーで機能を追加していくという戦略をとっています。ユーザーはシンプルなアプリケーションの場合、小さいサイズで使い始めることができ、アプリケーションを大きくしたい時に初めて機能を追加していくことができます。

軽量だと何がいいかと言うと、Cloudflareなどの環境で立ち上がりが速いので、パフォーマンスが上がるということです。また当然ながらアプリケーションをどこかへ移動する際にファイルサイズが小さいので短い時間で済みます。小さいことはいいことです。

3. 速い

Honoは速いです。Node.js上の比較ですが、Expressに比べてHonoは8倍速いです(出典)。

Fast

Honoではとにかく「やることを少なくする」方針をとっています。当然だと思うかもしれませんが、下のコードがCloudflareやDeno、Bunで一番速いコードです。

// 一番速いアプリケーション
export default {
  fetch: () => {
    return new Response('Hi')
  }
}

Honoではこのコードに極力近づけるようにしています。つまり以下のフローだけにします。

  1. リクエストを受け取る
  2. 適切なハンドラにルーティングする
  3. ハンドラがレスポンスを返す
  4. アプリケーションがそれを返す

これだけです。ですので、高速に動きます。

また、ルーティングは必要になりますが、高速なルーターを搭載しています。Honoでは現在5つのルーターを使うことができます。ここまでルーターにこだわったフレームワークはありません。

Routers

なかでもRegExpRouterはJavaScript界で最も速いルーターの一つです。例えば、Expressで使われていたpath-to-regexpというルーターは登録されたルートに対してリクエストパスを頭からそれぞれ正規表現でマッチさせます。つまり、5個ルートが登録されていたら最大5回正規表現のマッチが行われるわけです。

Linear Router

一方RegExpRouterは登録されたルートをひとつの大きな正規表現にして、一発でマッチさせます。そのため非常に高速です。

RegExpRouter

また高速化するためにクエリパーサーなどを自作しています。

Honoは軽さと速さにこだわったフレームワークといえます。

4. 開発者体験

Honoでは開発者体験を重視しています。

ユーザーには"Easy"なインターフェースを提供しています。もしJSONを返したければc.json()を実行するだけです。

import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.json({
    message: 'How are you?'
  })
})

export default app

特に他のファイルからメソッドをimportする必要はありません。短い行数で書くことができます。これは「簡単なことは簡単にできる」というHonoらしさであります。そのため初心者にとってHonoを使いはじめるハードルは非常に低いです。そしてより複雑なことをしたければ後述するミドルウェアとアダプターを使っていくことができるのです。

加えて、TypeScriptの強力なサポートが特徴です。例えばパスパラメータの取得時にc.req.param()に型がつきます。

c.req.param

レスポンスにはどんなヘッダーがあるかどうか?もTypeScriptの型補完で調べなくても分かったりします。

headers

また型をサポートしたZodやValibotなどのスキーマバリデータをバリデーションに用いることができ、その際に型情報を上手に扱えます。以下のコードではc.req.valid()の返却値に型がきれいにつきます。

Zod Validator

Zod Validator

Honoの実装はシンプルですが、TypeScriptでリッチな開発体験を足しています。

さらに興味深いのはJSXをサポートしている点です。以前、Honoの開発当初では、HTMLを作るのにMustacheというテンプレートエンジンを使える機能がありました。それは悪くなかったのですが、他のテンプレートエンジンではevalを使うものが多く、そうするとCloudflare Workersなどの環境で動きません。そこで考えられたのがJSXを使うというものです。これまでReactでよく使われていたJSXをサーバーサイドのみで使うというアイデアは面白いものです。

実際にJSXを使うにはtsconfig.jsonを適切に設定して、拡張子をtsxにして、JSXをc.html()に渡すだけです。主要なエディタはJSXをサポートしているのでHTMLを書くという体験が非常によいです。

import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.html(
    <html>
      <body>
        <h1>Hello, Hono with JSX!</h1>
      </body>
    </html>
  )
})

export default app

アプリケーションをテストをする際の体験も優れています。Honoのアプリケーションはサーバーを立ち上げずにリクエストを送り、レスポンスを試験することができます。Honoのインスタンスにはapp.request()というメソッドが生えています。それを使うと簡単にHonoアプリにリクエストを送ることができて、Responseオブジェクトを取り出せます。これを利用してテストを書けるわけですね。例えば、あるルートのレスポンスが200どうかを試験するにはこのように書きます。

describe('Simple test', () => {
  it('should return 200 response', async () => {
    const res = await app.request('/')
    expect(res.status).toBe(200)
  })
})

これは内部的にリクエストを送っているだけなので、実際のサーバーを立ち上げていません。それでも十分テストできます。honojs/honoのテストの多くはこの方法を取っています。

5. ミドルウェア

Honoの開発ではミドルウェアを使って機能を拡張していくことができます。

Honoではミドルウェアとハンドラ、つまりレスポンスを返すメソッドは同じようなものです。レスポンスを返したらハンドラになります。つまり以下はハンドラです。

app.get('/', (c) => {
  return c.text('Hi') // returning Response
})

このハンドラがレスポンスを返す前後に作用するのがミドルウェアです。await next()の部分がハンドラと考えてください。例えば以下のような例です。

app.get('/', async (c, next) => {
  console.log('before handler')
  await next()
  console.log('after handler')
})

実用的な例では、全てのパスに対するGETリクエストのレスポンスのレスポンスヘッダーを追加するミドルウェアはこのようにかけます。

app.get('*', async (c, next) => {
  await next()
  c.header('X-Message', 'from middleware')
})

こうしてユーザーが自身で書くミドルウェアのことをカスタムミドルウェアと呼んでいます。他にHonoにはビルトインミドルウェアと3rdパーティミドルウェアがあります。ビルトインミドルウェアと3rdパーティは予め用意されているので、ユーザーが自分で作らなくても使えます。

ビルトインミドルウェアはhonoパッケージに同封されているミドルウェアのことで、外部ライブラリに依存せず、Webアプリ開発でよく使う機能を提供しています。現在のビルトインミドルウェアは以下の22個になります。

  • Basic Authentication
  • Bearer Authentication
  • Body Limit
  • Cache
  • Combine
  • Compress
  • Context Storage
  • CORS
  • CSRF Protection
  • ETag
  • IP Restriction
  • JSX Renderer
  • JWK
  • JWT
  • Logger
  • Language
  • Method Override
  • Pretty JSON
  • Request ID
  • Secure Headers
  • Timeout
  • Timing

たとえばBasic認証を一から実装するのは面倒なのですが、ビルトインミドルウェアとして使うことができます。hono/basic-authからbasicAuthメソッドをインポートして、ルートのハンドラの場所に置くだけです。以下では/admin配下のパスに認証をつけています。

import { basicAuth } from 'hono/basic-auth'

//...

app.use(
  '/admin/*',
  basicAuth({
    username: 'my-name',
    password: 'my-password'
  })
)

3rdパーティミドルウェアは、honoコアに含まれていないミドルウェアのことを呼んでいます。honoコアは外部のライブラリに依存しないのですが、3rdパーティミドルウェアは外部のライブラリに依存することが多いです。GitHubのhonojsオーガニゼーション配下にあるhonojs/middlewareで開発され@hono/*名前空間で配信されているものもあれば、作者が独自の名前で公開しているものもあります。

honojs/middlewareは巨大なモノレポになっていて、その下にたくさんの3rdパーティミドルウェアがあります。以下はその一例です。

認証

バリデータ

モニタリング

その他

例えば、UA Blockerミドルウェアは指定したユーザーエージェントをブロックするミドルウェアです。

import { uaBlocker } from '@hono/ua-blocker'
import { Hono } from 'hono'

const app = new Hono()

app.use(
  '*',
  uaBlocker({
    blocklist: ['ForbiddenBot', 'Not You'],
  })
)
app.get('/', (c) => c.text('Hello World'))

export default app

このようにhonoコアにあるビルトインを依存なしで使うこともできるし、3rdパーティミドルウェアを別途インストールして使うこともできるし、もし希望するものがなければ自分でカスタムミドルウェアを作ればいいのです。

6. ヘルパー

honoパッケージではビルトインミドルウェアの他に便利なヘルパーが提供されています。

例えば、HTTPストリーミングのレスポンスを返すのに便利なのがStreamingヘルパーです。最初にHelloとテキストを表示して1秒後にHono!と表示するエンドポイントを実装するにはこのように書けます。

import { Hono } from 'hono'
import { streamText } from 'hono/streaming'

const app = new Hono()

app.get('/', (c) => {
  return streamText(c, async (stream) => {
    await stream.writeln('Hello')
    await stream.sleep(1000)
    await stream.write(`Hono!`)
  })
})

export default app

現在提供されているヘルパーは以下の15個です。

  • Accepts
  • Adapter
  • ConnInfo
  • Cookie
  • css
  • Dev
  • Factory
  • html
  • JWT
  • Proxy
  • Route
  • SSG
  • Streaming
  • Testing
  • WebSocket

このヘルパーもミドルウェアもhonoとは別のファイルからimportします。ですので、使った時に始めてバンドルされるわけです。ですので、使わなければバンドルされないというのがポイントです。コアを小さく保ったまま、機能を拡張することができます。

7. アダプター

最後のコンセプトはアダプターです。Honoはあらゆるランタイムで動くと言いました。しかし、ランタイムもしくはプラットフォームごとに微妙に異なる点があるので、それを吸収するのがアダプターの役目です。

Adapters

例えば、HonoをAWS Lambda上で動かすにはそのままの書き方では動きません。そこでhonoに同封されているアダプターを使います。handleというメソッドにHonoのインスタンスを渡してhandlerという名前でexportすればAWS Lambada上で動くようになります。

import { Hono } from 'hono'
import { handle } from 'hono/aws-lambda'

const app = new Hono()

app.get('/', (c) => c.text('Hello Hono!'))

export const handler = handle(app)

また、HonoアプリをNode.jsで動かしたければ@hono/node-serverというアダプタを使います。

import { Hono } from 'hono'
import { serve } from '@hono/node-server'

const app = new Hono()

app.get('/', (c) => {
  return c.text('Hi')
})

serve(app)

ベースのアプリケーションがあって、異なる環境に対応するためにアダプタを要所に使うというアプローチは、とても便利です。例えば、上記の2つのアダプタを使ったユースケースで、ローカル環境ではNode.jsで開発し、AWS Lambdaに公開する際にはAWS Lambdaアダプターを使用するというのが、ファイルをうまく分割すればできます。

まずベースとなるアプリをsrc/app.tsに作ります。

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

const app = new Hono()

app.get('/', (c) => {
  return c.text('Hi')
})

export default app

そして、このアプリケーションをインポートして、Node.jsアダプタを使った開発用のエントリポイントを作ります。

src/entry-dev.ts
import app from './app'
import { serve } from '@hono/node-server'

serve(app)

これを立ち上げるにはtsxを使う場合このようにして開発に使えます。

tsx --watch src/entry-dev.ts

そして、本番用ではAWS Lambdaアダプターを使います。

src/entry-prod.ts
import app from './app'
import { handle } from 'hono/aws-lambda'

export const handler = handle(app)

このファイルをデプロイする時にエントリーポイントで指定すればいいのです。ちなみにsrc/app.tsは何もせずともCloudflare Workers、Deno、Bunで動きます。

アダプターを使うことで、アプリケーションを変えずに各ランタイム、プラットフォームに対応させることができます。

まとめ

以上、Honoの7つのコンセプトについて語ってきました。

7 concepts

それぞれよく考えられていると思います。これがHonoらしさです。この機会にHonoについて深く知ってもらい、より使うモチベーションになったとしたら幸いです。

Hono

Discussion