目次
はじめに
こんにちは、「拳で」 と申します!
NRI OpenStandia Advent Calendar 2024の9日目は
【時代はHono🔥!?】今さらながらNext.js App RouterユーザがHonoを調べてみた
というタイトルでお送りいたします!
最近Honoについての記事や情報をよく見かけるようになり、全く調べたり触ったりしたことがなかったのでこの機会に調べてみました。
Next.js App Routerを使って、3案件ほどこなしたことがあるのでNext.jsユーザ目線で調べて思ったことなども書きました。
想定読者
この記事は以下のような方を想定して書いています。
- Honoのこと、全然知らないけど概要を知りたい
- Honoの2024年時点での立ち位置を知りたい
- Honoを使っていて、複数エンドポイント(Grouping routes)・OpenAPI Docs対応・RPCモード対応したサンプルコードが見たい
TL;DR
- Honoは2024年に特に伸びてきている
- Honoは軽量・高速で開発体験が優れたフレームワークで、特にエッジやバックエンド開発に最適
- Expressの代替になる
- Honoの開発者体験は良い
Honoを使ってみたで紹介したコードは、以下リポジトリで公開しています💡
- Next.jsと比較し、両者フルスタックフレームワークだがHonoはバックエンド(API)開発や軽量・ポータビリティに強み
- 比較表
- Hono・Next.jsで両者補完する構成も良い
- Expressの代替やエッジでのプロキシ用途、Next.jsとの併用などがユースケースとして向いてそう
Honoとは
Honoの概要
HonoはJavaScriptのWebフレームワークです。
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Node.js!'))
serve(app)
上記サンプルのようにExpressに似た書き味で、WebAPIの開発ができます。
※v3.1.0以降はJSXもサポートして、フルスタックフレームワークになったようです!
比較のため、Expessのサンプルコードも載せておきます。
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
Cloudflare内はもちろん、CyberAgent, Prismaなど様々な組織・団体で実利用されているようです。
以下は、Honoを利用していることを公表している組織・団体の情報です。
コラム:Honoの成り立ち
- Yusuke Wadaさんが開発される、日本発のフレームワークです
- 開発当初は、Cloudflare Workers(エッジ環境)に特化した「Webアプリを作るためのフレームワーク」をコンセプトに開発されています
- Hono[炎]は、Cloudflareのflare(炎)とかけたネーミングが由来
- 開発初期のYusuke Wadaさんの記事:Hono[炎]っていうイケてる名前のフレームワークを作っている
Honoの特徴
Honoは以下のような特徴があります。
- 爆速:JavaScriptのWebフレームワーク内で最速
- 軽量:Minify すれば 14KB 以下 (Express は 572KB) 1
-
ルーター:5種類のルーターを持ち、目的に応じて使い分けできます。特にRegExpRouterはJavaScript界隈で最速のルーターです
- Hono v4で登場した、HonoとViteを組み合わせたメタフレームワーク HonoXではFile-based Routingも提供されます
-
Web標準:HonoはWeb 標準のみを使用して実装されています。 Web 標準 API をサポートするあらゆるランタイムで動作
- Cloudflare Workers, Fastly Compute, Deno, Bun, Vercel, Netlify, AWS Lambda , Lambda@Edge, Node.js などで動作
- ミドルウェア & ヘルパー:シンプル・軽量なコアに、豊富なミドルウェア・ヘルパーを適宜組み込むことで機能追加可能
- 開発体験:TypeScriptによる型のサポートや、RPCモードによる型安全なAPIクライアントの生成が簡単にできます
開発体験を意識した機能が豊富なので好印象...!
Trends
Express is the new JQuery
という界隈で一時話題になったポストや
Express is the new JQuery
— Ben Holmes (@BHolmesDev) July 14, 2023
I Stopped Using Express.js: Because Bun and Hono 🔥
と題して、Expressではなく、Honoを使いだしたという方も登場してきており
Honoの台頭とともに、脱Expressする人も増えてきている??気がしてます。
「拳で」の体感では 2024年になってよくHonoの記事や事例を見聞きするようになったのですが、トレンドはどうなってるか調べてみました。
今回各ツールのトレンドを比較するため
の調査を行いました。
比較対象は以下です。
プロダクト名 | Stars |
---|---|
Express | |
Fastify | |
Elysia | |
Next.js |
GitHub Star History
GitHub Star Historyで各GitHubリポジトリのStar数推移をチェックしてみました。
引用:GitHub Star History | Hono vs express vs fastify vs elysia vs Next.js
2022年に登場したHonoはかなりの勢いでStar数を増やしてますね!
特に2024年の加速は目覚ましいものがあり、よくHonoの記事や事例を目にするのも納得がいきました。
npm trends
node modulesとして配布されている、モノレポ管理ツールのダウンロード数をnpm trendsで比較しました。
expressやNext.jsを比較対象に加えると他のプロダクトのグラフが潰れてしまったので除外しています。
やはり、2024年から特にダウンロード数が増えてますね
引用:npm trends | elysia-vs-fastify-vs-hono
State of JavaScript
State of JavaScript 2023調査結果から回答者が普段使っているBack-end Frameworksのランキングでは全体12位という結果でした。
回答者の1%が使っているという状況でしたが
- 回答の選択肢にHonoがなかった
- 「その他」の自由入力欄にHonoを記入した人が一定数いた
というのも少し影響している気がします。
State of JavaScript 2024のSurveyでは回答選択肢にHonoが追加されており、今年の躍進を見る限りランキングの大幅アップも期待できそうですね!
JavaScript OpenSource Award
JavaScript OpenSource Award 2024のThe Most Exciting Use of Technology部門にノミネートされていました!
惜しくも受賞とはならなかったようですが、Honoは2024年のJavaScript界隈で注目を集めたプロダクトの1つですね!
Honoを使ってみた
前置きが長くなりましたが、早速Honoを使ってみましょう!
コードは以下リポジトリで公開しています💡
複数のエンドポイントを定義した、Grouping routes構成の上で
- OpenAPI Docs対応
- RPCモード対応
しているサンプルになるので、よかったらぜひご覧ください
※Grouping routesで上記2つに対応しているサンプルコードをあまり見つけきらなかったので、お役に立てれば幸いです..
Geting Started
を参考にプロジェクトを作成します。
npm create hono@latest my-app
このコマンドを打つと、インタラクティブなCLIでプロジェクトが作成できます。
便利!
どのランタイムで動作させるか、テンプレートを選択できます。
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Qiita AdventCalendar 2024!')
})
const port = 3000
console.log(`Server is running on http://localhost:${port}`)
serve({
fetch: app.fetch,
port
})
テンプレートから生成されたコードをそのまま実行して、用意されたエンドポイントにcurlしてみます。
curl -v localhost:3000
* Host localhost:3000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:3000...
* Connected to localhost (::1) port 3000
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< content-type: text/plain; charset=UTF-8
< Content-Length: 31
< Date: Wed, 04 Dec 2024 14:53:25 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
Hello Qiita AdventCalendar2024!
returnに設定されたテキストが返ってきますね!
記述はExpressライクな印象
RPCモード
次に個人的に、非常に注目しているPRCモードを試してみます。
RPCモードとは
RPCモードとは以下のような機能です。
- Web APIの仕様、特にインプット・アウトプットをサーバーとクライアント間で共有するためのもの
- OpenAPIやgRPCを使ってやりたかったことを叶えるかもしれない
- サーバーとクライアントをどちらもTypeScriptで書くことが大前提である
- 同種のものにtRPCがあるが、Honoの場合、普通のREST APIを書くだけで使える
- クライアントはfetchのラッパーであり、スタンダードなResponseオブジェクトを扱う
- いわゆる「型安全」を提供するものであり、エディタの補完がバチバチに効く
ざっくりいうと
今までOpenAPI Specからopenapi-typescriptとかを使って作っていた、型安全なAPI ClientをHonoでAPI書くだけで簡単に実現できる機能です。
これがめっちゃ便利...!
server
ひとまず、普通にAPIを作成します。
メソッドチェーンで定義 すると、RPCモードが使えるようになります。
ついでに、zodによるpost valueのバリデーションチェックも加えます。
import { serve } from '@hono/node-server'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { z } from 'zod'
const app = new Hono()
const schema = z.object({
name: z.string().min(1),
country: z.string().min(1),
})
const routes = app
.get('/', (c) => {
console.table(c)
return c.text('Hello Qiita AdventCalendar 2024!')
})
.get('/api/users', (c) => {
return c.json({
user: 'foo-users',
})
})
.post('/api/users', zValidator('json', schema), (c) => {
const { name, country } = c.req.valid('json')
return c.json({
message: `Hello, ${name}. Your country is ${country}.`,
})
})
const port = 3000
console.log(`Server is running on http://localhost:${port}`)
serve({
fetch: app.fetch,
port,
})
// routesの型を取り、exportしておく
export type AppType = typeof routes
export default app
curlでデータをpostしてみます。
❯ curl -X POST localhost:3000/api/users -H 'Content-Type: application/json' -d '{"name":"jong","country":"japan"}'
{"message":"Hello, jong. Your country is japan."}
postしたデータが正しく受信できてそうです!
次にあえてバリデーションチェックエラーを起こしてみます。
❯ curl -v -X POST localhost:3000/api/users -H 'Content-Type: application/json' -d '{"name":"jong"}'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Host localhost:3000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:3000...
* Connected to localhost (::1) port 3000
> POST /api/users HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.7.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 15
>
* upload completely sent off: 15 bytes
< HTTP/1.1 400 Bad Request
< content-type: application/json; charset=UTF-8
< Content-Length: 163
< Date: Wed, 04 Dec 2024 15:23:34 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
{"success":false,"error":{"issues":[{"code":"invalid_type","expected":"string","received":"undefined","path":["country"],"message":"Required"}],"name":"ZodError"}}
HTTPステータス 400のレスポンス定義はしてませんが、よしなにステータスコードやエラーメッセージをレスポンスしてくれて便利😍
zodのschemaでmin(1)
していすれば、requireに指定できますね。
client
server側で生成したAPIの型をhc
に与えることで、型安全なAPI Clientを生成できます。
しっかり、エディタの型補完の恩恵を受けることができます!!
これは簡単・便利ですね!
import { hc } from 'hono/client'
import type { AppType } from './server'
// TypeSafeなAPI Clientを生成
const client = hc<AppType>('http://localhost:3000/')
const res = await client.api.users.$post({
json: {
name: 'Qiita',
country: 'japan',
},
})
if (res.ok) {
const data = await res.json()
console.log(data)
}
❯ npx tsx src/client.mts
{ message: 'Hello, Qiita. Your country is japan.' }
問題なく、使えます!
こんなサクサクとTypeSafeなAPI Clientが作れるのは素敵...
参考
Custom fetch method
hc
で生成されるAPI Clientはfetchなので、必要に応じてAPI Clientをカスタマイズできます。
先程のコードにカスタムヘッダを追加してみます。
import { hc } from 'hono/client'
import type { AppType } from './server'
// TypeSafeなAPI Clientを生成
const client = hc<AppType>('http://localhost:3000/', {
fetch: (input: RequestInfo | URL, requestInit?: RequestInit) => {
const headers = new Headers(requestInit?.headers) // 既存のヘッダを保持
headers.set('x-qiita-custom-header', 'qiita adventcalendar 2024') // カスタムヘッダを追加
return fetch(input, {
...requestInit,
headers, // 更新されたヘッダをセット
})
},
})
const res = await client.api.users.$post({
json: {
name: 'Qiita',
country: 'japan',
},
})
if (res.ok) {
const data = await res.json()
console.log(data)
}
サーバ側でカスタムヘッダを受信できてます!
> dev
> tsx watch src/server.ts
Server is running on http://localhost:3000
{
accept: '*/*',
'accept-encoding': 'gzip, deflate',
'accept-language': '*',
connection: 'keep-alive',
'content-length': '34',
'content-type': 'application/json',
host: 'localhost:3000',
'sec-fetch-mode': 'cors',
'user-agent': 'node',
'x-qiita-custom-header': 'qiita adventcalendar 2024' //custom header
}
RPCモードの利用について
tRPC vs ts-rest vs Hono RPC
の記事にもあるとおり、モノレポや同一プロジェクトでない場合RPCモードを使うのは工夫が必要です。
※Honoを使ってAPIを構築するプロジェクトが別リポジトリの場合、package化してroutesの型を別リポジトリにimportしたりが必要
OpenAPI
HonoはOpenAPI形式でスキーマを定義することで、APIドキュメントの生成も簡単にできます。
こちらの記事が大変参考になりました
以下のようにOpenAPI形式でrouteを定義すると
import {
ErrorSchema,
UserGetResSchema,
UserPostResSchema,
UserReqSchema,
} from '@/schema/web/userSchema'
import { createRoute } from '@hono/zod-openapi'
export const userGetRoute = createRoute({
method: 'get',
path: '/users',
responses: {
200: {
content: {
'application/json': {
schema: UserGetResSchema,
},
},
description: 'Returns a sample user name.',
},
},
})
export const userPostRoute = createRoute({
method: 'post',
path: '/users',
request: {
body: {
content: {
'application/json': {
schema: UserReqSchema,
},
},
},
},
responses: {
200: {
content: {
'application/json': {
schema: UserPostResSchema,
},
},
description: 'Returns a posted user name, country.',
},
400: {
content: {
'application/json': {
schema: ErrorSchema,
},
},
description: 'Bad Request',
},
},
})
import { webRouter } from '@/routes/web'
import { serve } from '@hono/node-server'
import { swaggerUI } from '@hono/swagger-ui'
import { OpenAPIHono } from '@hono/zod-openapi'
import { logger } from 'hono/logger'
import { prettyJSON } from 'hono/pretty-json'
const baseApp = new OpenAPIHono().basePath('/api')
baseApp.use('*', prettyJSON())
baseApp.use('*', logger())
// OpenAPIドキュメントの生成
baseApp
.doc31('/doc', {
openapi: '3.1.0',
info: {
title: 'API',
version: '1.0.0',
},
})
.get(
'/ui',
swaggerUI({
url: '/api/doc',
}),
)
const app = baseApp.route('/web', webRouter)
const port = 3000
console.log(`Server is running on http://localhost:${port}`)
serve({
fetch: app.fetch,
port,
})
export type AppType = typeof app
簡単にSwagger UIのAPIドキュメントを生成できます!!
便利すぎる...
もちろん、RPCモードも使えます。
Next.jsとの比較
HonoとNext.jsはどういった関係性なのか比較してみました。
成り立ち
Yusuke Wadaさんが以下のようにポストしてます。
HonoでやりたいのはNext.jsがReactというフロントエンドの文脈から出てきたのに対して、我々はサーバーサイドからExpressの代替としてWeb Standardsを武器に攻めるというチャレンジである。
— Yusuke Wada (@yusukebe) January 31, 2024
引用:Hono v4より
個人的に以下のような印象を受けます。
- Hono:Expressの代替や、エッジ環境でのAPI開発目的が出発点
- バックエンド側の機能が充実。エッジ環境やマルチランタイム対応に強み。シンプルで軽量なフレームワーク
- Next.js:Reactのメタフレームワークとして、画面開発目的が出発点
- フロントエンド側の機能が充実。バックエンドの機能も含め、様々な機能を有する重厚なフレームワーク
HonoとNext.jsの機能比較
ざっくりですが、HonoとNext.jsの機能や特徴を比較してみました。
※誤りがあったら申し訳ございません
こう並べてみると、Honoもフルスタックフレームワークに位置付けられますね。
- Honoは柔軟なミドルウェアを駆使したAPI開発に強み
- Next.jsは画面系の開発に強み
といった印象を受けます。
双方のフレームワークの出自のとおりですね。
HonoとNext.jsの関係性
今のところ、両者がカチ合って直接の競合になるイメージは湧きません。
- API開発やエッジでの処理が主体のアプリケーション:Hono
- フロント系の開発が主体のアプリケーション:Next.js
という棲み分けです。
両者は補完関係にもあるので、美味しいとこ取りする構成は魅力的です。
参考
ユースケース
「拳で」がHonoについて調べながら、このユースケースが向いてるなと個人的に感じたものを並べます。
1. Expressの代替
まず、ぱっと思い浮かんだのはこちらです。
既存のExpressプロジェクトのリプレースは大変だと思うので、新規案件でAPIをTypeScriptで書く場合は選択肢に入れても良さそう。
2. Lambdalith
AWS Lambda・Lambda@Edgeを使うケースでLambdalith構成を選択する場合、マルチランタイム対応しているHonoは選択肢に入ると思います。
AWS管理ランタイムを気にしなくて良くなる恩恵はあるかなと。
参考
3. エッジでのプロキシ
Yusuke Wadaさんの資料で紹介されていますが、エッジ上で
- レスポンスヘッダーのハンドリング
- CORS対応
- キャッシュ
など様々な用途で、Honoは活躍すると思います。
詳細は、資料参照
4. Next.jsのRoute Handler内での利用
Next.jsでRoute Handlerを使う構成を考えている場合、Route HandlerでHonoを採用する のはかなりアリな構成だなと思いました。
RPCモードで型安全なAPI Clientを生成し、Next.jsの画面側のコードでそれを使う とアプリケーションの品質を高めることができそうです。
Honoを採用しておくと
Route Handler内でCPU boundな処理によって、プロセスがハングしてNext.jsの画面応答もしなくなる ような性能問題に直面したとき、Route Handler(= API)部分だけ後々分離したりできます。
※そもそもNodeで、CPU boundな処理するなって話はありますが...😅
Node.jsでCPU boundな処理をすると...の件は、PLAIDがNode.jsを採用し、5年間で12万行書いてわかったことが参考になります。
参考
5. BFF
引用:流行りのBFFアーキテクチャとは?|Offers Tech Blogより
BFFは一般的に広く公開するのではなく、特定のアプリケーションのために構築するので個人的にOpenAPI specをわざわざ書くのはだるいなーと思ってました。
HonoならRPCモードで活用ができて、簡単にOpenAPIドキュメントが作れるので心理的ハードルは低いと感じました。
6. エッジでAPI + 簡単な画面を返したいとき
JSX対応やHonoXでファイルベースルーティングにも対応し、フルスタックWebアプリケーションフレームワークとしての活用もできそうです。
エッジ側でも簡単な処理や画面のレスポンスなら行けるので、軽量フレームワークとして...
所感
Honoを調査してみて、以下を感じました。
-
シンプル・軽量で開発者体験も非常に良い
- 特に、OpenAPI・RPCモードは非常に良い
-
マルチランタイム対応しており、ポータビリティ性が高いのはありがたい
- Next.jsのRoute Handlerに組み込んだりとか使える範囲が広い
-
公式ドキュメントがよりリッチになると嬉しい
- middlewareのオプションなどは網羅的に書かれてるわけではない
- middlewareのリポジトリ側では、情報が提供されているので大きな問題ではないが
- RPCモードは便利だが、モノレポ・同一プロジェクトでないと使いづらいので大規模プロジェクトでどう活用するかは工夫が必要??
実案件で使ってみたいと思いました。
ひとまず、趣味プロジェクトやハッカソンとかで使ってみるかな..🤩
まとめ
Honoについて色々と調査した内容をまとめさせていただきました。
シンプルながら奥深く、様々なユースケースが挙げられるのでこれから更に注目していきたいと思います
参考資料
- Hono公式ドキュメント
- Hono[炎]っていうイケてる名前のフレームワークを作っている
- Honoの今の状況
- Hono v4
- Honoの来た道とこれから 文字版!
- Honoの概要とその特徴: Web標準に従った軽量高速フレームワーク
- 覚書:Honoとは?次世代フレームワークが注目される理由~その魅力と可能性~
- Honoを使い倒したい2024