TypeScript初心者の私がHonoでバックエンドサーバー構築してみた ~RPCからテストまで~
TypeScriptとHonoで作る最小構成のAPIサーバー
はじめに
私は、フロントエンドエンジニアのゆず(@yuzunosk55)です。
この記事では、TypeScriptを使ってAPIサーバーを構築する方法を解説します。
フレームワークには、最近注目を集めているHonoを使用します。
主な内容:
- APIサーバーの基本的な構築方法
- Honoの特徴的な機能であるRPCの実装
- APIドキュメントの自動生成(ZodとSwaggerUIを使用)
※ 私もHonoを学び始めたばかりです。もし誤りなどありましたら、コメントでご指摘いただけると幸いです。
1. この記事について
こんな方におすすめです
- WebAPIの作り方を学びたい方
- TypeScriptでサーバーサイド開発に挑戦したい方
- HonoのRPCどうやるのか気になっている方
- APIのドキュメント作成を自動化したい方
前提知識
- JavaScriptの基本的な文法
- npmやnodeを使った開発の経験
- TypeScriptの基礎的な理解
この記事では扱わない内容
- TypeScriptの詳細な説明
- Git周りの説明
- 各ライブラリの詳細な仕組み
より詳しい情報は、記事内で紹介する公式ドキュメントをご参照ください。
2. 技術スタックの解説
Honoとは?
日本人の和田裕介さんがつくったTypeScript(JavaScript)で開発できるWebアプリケーションフレームワークです。
Hono = 「炎」という意味で元々Cloudflare Workers上で動作する良いフレームワークがなかったため、開発をスタートしたみたいです。
flareに対して作っていたのでHonoという名前になったようです。
なぜExpressではなくHonoを選ぶのか?
私の経験からExpressとHonoを比較してみたいと思います!
Expressの特徴
Expressは非常にミニマルなフレームワークです。
最初にExpressのドキュメントを見たときは驚きました。
本当にフレームワークなのかと思うほど、公式ドキュメントが薄いのです。
理由は、Expressの設計思想にあります:
- シンプルなフレームワーク
- 必要な機能は全てプラグインとして追加する
- 開発者が自由に設計できる
(余談:これほどにシンプルで小さいのは、RubyのフレームワークSinatora
に影響を受けているからだとWikiに記述されていました。https://ja.wikipedia.org/wiki/Express.js)
この自由度の高さは、諸刃の剣のように感じました。
例えるなら…
「広大な平原に降ろされて、『はい、家を建ててください!』と言われているような感じ」です。
経験や知識がないと、どこから始めれば良いのか途方に暮れてしまいます。
Honoの魅力
そんな時に出会ったのがHonoです!
Honoの公式サイトには、こんな言葉が書かれていました。
Batteries Included
Hono has built-in middleware, custom middleware, third-party middleware, and helpers. Batteries included.
これは重要なポイントで:
- 必要な機能が最初から揃っている
- 追加のインストールが少なくて済む
- 公式ドキュメントを読めば、構築に必要な情報が手に入る
さらに、Honoには以下のような利点もあります:
- TypeScriptとの相性が抜群
- RPCによる型安全な通信が可能
- 基本的な書き方がExpressとほぼ同じなので、学習コストが下がる。
- Honoに機能が内包されていることで、package.jsonの記述量が減る。
これらの利点に魅力を感じ、私はHonoを学び始めました。
その他、構築に使われている技術の簡易説明
Bunの特徴と利点
Node.jsの代替となる新しい実行環境です。実行環境としてだけでなく色んなことができます。
とにかく速い!!まだすべてのnpmパッケージに対応できてないようですが、速すぎて開発体験爆上りします。
- Node.jsの代替となる
超
高速な JavaScript/TypeScript実行環境。 - パッケージマネージャーとしても使える(npm/yarnの代替)。超早い。
- テストも出来る。
Zodで型安全性が確保できる
TypeScript向けのスキーマ検証/バリデーションに使えるライブラリです。
- TypeScriptの型が自動生成できる。
- 直感的に書ける(例:z.string()、z.number())。
OpenAPIとSwagger UIの役割
APIの仕様書を自動生成するのに使うツールです。
OpenAPI
- APIの仕様を記述するための標準規格。
- これを使ってコーディングしていくだけで、コードがドキュメントを自動生成してくれる。
- API修正時に、ドキュメントを修正する手間がなくなる。
Honoには、ZodとOpenAPIを使うためのパッケージも用意されている。
SwaggerUI
- APIドキュメントをブラウザで確認できるツール。
Honoには、SwaggerUIを使うためのパッケージも用意されている。
HonoのRPC機能
RPCを使うと、以下のような利点があります。
- 型安全性向上
- APIのリクエスト/レスポンスの型チェック
- 入力補完による開発効率の向上
- APIの呼び出しがシンプルになる。
型情報がある場合とない場合の比較イメージ
型情報がない場合
// サーバー側
app.post('/api/user', (c) => {
const body = c.req.body // 型が不明確なので、指摘できない
return c.json({ id: 1, name: body.name })
})
// ------------------
// クライアント側(別サーバー)
const response = await fetch('/api/user', {
method: 'POST',
// 本当はageが必要だったとしても、指摘されない
body: JSON.stringify({ name: "太郎" })
})
const data = await response.json()
型情報がある場合
// 共有する型定義
type User = {
name: string;
age: number;
}
// サーバー側
app.post('/api/user', (c) => {
const body = c.req.valid<User>() // ここで型チェック!
return c.json({ id: 1, ...body })
})
// ------------------
// クライアント側(別サーバー)
const client = hc<typeof app>('/api') // サーバーの型情報を共有
// APIリクエストがシンプル、型安全になる!
const result = await client.api.user.$post({
json: {
name: "太郎", // OK
age: 20 // OK
invalid: true // エラー!この項目は定義されていない
}
})
これを改善するために、フロント側ではAPI側から型情報を取得する必要がある
型の情報をAPI、フロント、ドキュメント全部一致させる必要があるのだけど、これがなかなか手間…
しかしHonoのRPC機能はとても簡単にこれが出来る!!!(すごい!)ということ。
3. 開発環境のセットアップ
1. Bunのインストール
Bunを使ったセットアップ方法も公式に用意されているので、基本はこれ通りに進めれば大丈夫です。
まずは、Bun公式にあるコマンドでBunをインストール。
curl -fsSL https://bun.sh/install | bash
2. Honoアプリケーションの作成
Honoには、Bun用のアプリケーション作成コマンドが用意されています。
いくつか対話式の質問が表示されるので、以下のように答えてください。
質問1: Which template do you want to use?
回答 : bun
質問2: Do you want to install project dependencies?
回答 : yes
質問3: Which package manager do you want to use?
回答 : bun
✓ bun create hono@latest my-app ← Bun用のアプリケーション作成コマンドを実行
create-hono version 0.15.3
✔ Using target directory … my-app
? Which template do you want to use? bun
? Do you want to install project dependencies? yes
? Which package manager do you want to use? bun
✔ Cloning the template
✔ Installing project dependencies
🎉 Copied project files
Get started with: cd my-app
成功すると、 cd my-appでディレクトリ移動するように言われます。
3. パッケージインストール
作成したプロジェクトに移動し、必要なパッケージをインストールします。
(余談: --dry-run
をoptionで付けると、実際にはインストールせずに実行結果を確認できます。)
cd my-app
✓ bun install
bun install v1.0.36 (40f61ebb)
Checked 6 installs across 7 packages (no changes) [55.00ms]
4. サーバー起動
一度サーバーが起動するか確認しておきましょう。
bun run dev
Hello Honoと表示されてたらOK
package.json
のnpm scripts
に記述されているdev
コマンドで、サーバーをホットリロード付きで起動させてくれています。そのため、ファイルの変更が即座に反映されます。
5. ts-configを修正
Bunの公式にTypeScriptおすすめ設定があるので、これを使います。
(config設定が理解できている方は、自由に設定してください。)
@types/bun
をインストールするよう書かれていますが、既にインストールされているので、スルーして大丈夫です。
この段階でGit管理しておきましょう
git init
$ git remote add origin {your_repository_URL}
git add .
git commit -m "Initial commit"
git push -u origin main
4. 最小構成のAPIサーバーを作ってみよう
1. プロジェクトの準備
必要なファイルの準備とnpm scripts
を更新します。
ファイルの作成
srcディレクトリ配下にserver.ts
とclient.ts
を用意します。
mv src/index.ts src/server.ts # リネーム
touch src/client.ts
ls src/
client.ts server.ts
package.jsonの更新
実行コマンドを更新します。
package.json
のnpm scripts
を更新します。
"scripts": {
"dev": "bun run --hot src/server.ts",
"client": "bun run --hot src/client.mts",
},
2. 超簡単なAPI作成
RPCを実装したいだけなので、最小で作ります。
※ この記事ではDB連携はしません。
ここで記述するコードの全体は以下になります。
import { Hono } from 'hono'
const app = new Hono() // これの下に記述
// 適当なテストデータ追加
const users = [
{id: 1, name: 'tarou', age: 15},
{id: 2, name: 'hanako', age: 20},
]
// API追加
app.post('/api/users', async (c) => {
const user = await c.req.json()
users.push({id: users.length + 1, ...user})
return c.json(user
})
簡単な説明
新しいHonoアプリケーションオブジェクトを作成しています。
const app = new Hono()
作成したHonoアプリケーションオブジェクトに対して.HTTP_METHOD
と記述することで、各メソッド、各パスでリクエストを受け付けられます。
下記の記述の場合/api/users
というエンドポイントに、POSTメソッドでリクエストを受け付けるAPIを作成しています。
リクエスト時に渡されてきたデータはc(context)
の中に入っています。
c.req.json()
と記述することでリクエストボディのデータをJavaScriptのオブジェクトにパースして取得します。
appメソッド
contextについて
APIを追加できたら、curlコマンドでAPIにリクエストを飛ばしてみましょう。
curl -X POST -H "Content-Type: application/json" -d '{"name": "yamada", "age":
25}' http://localhost:3000/api/users
3. 型情報のエクスポート
src/server.ts
のexport default app
してる行の上で、クライアント側で型情報を取得できるようexport
します。
//型を情報を出力したいルートを変数にいれる
const sampleRoutes = app
.post('/api/users', async (c) => {
const user = await c.req.json()
users.push({id: users.length + 1, ...user})
return c.json(user)
})
.get('/', (c) => {
return c.text('Hello Hono!')
})
// クライアント側で使えるようにexport
export type AppType = typeof sampleRoutes
export default app
簡単な説明
/api/users
と/
というルートの型情報を取得したい場合、それぞれのHTTP_METHODSをチェインさせます。
それを変数に格納しtypeof
を使って型を抽出しています。
公式でも、RPCを使う方法として記述されています。
typeofについて
※ 大規模なアプリケーションを構築する場合、下記を参考にしてください。
それぞれのエンドポイントでチェインさせたルートをexportすれば、RPC機能がつかえるはずです。
記述例
エンドポイントごとにファイルを分割
import { Hono } from 'hono'
const userRoute = new Hono().basePath('/users')
.post('/', (c) => {
return c.json({ message: 'user created' })
})
.get('/', (c) => {
return c.json({ message: 'users fetched' })
})
.delete('/:id', (c) => {
return c.json({ message: 'user deleted' })
})
.put('/:id', (c) => {
return c.json({ message: 'user updated' })
})
export default userRoute
親ファイルで読み込ませる
import { Hono } from 'hono'
import userRoute from './userRoute'
const app = new Hono()
// app.route()を使って、読み込むとapp.routesに追加できる
const route = app.route('/api', userRoute)
export type AppType = typeof routes
export default app
4. クライアントサーバー作成
APIを呼び出すクライアントを最低限のコードで作成します。
クライアントオブジェクト作成
まずは以下の記述を書いてください。
import type { AppType } from './server'
import { hc } from 'hono/client'
// hcの型にAppTypeを指定し、引数には、ホストのドメインを記述する
const client = hc<AppType>('http://localhost:3000/')
簡単な解説
サーバーからAppTypeという型がexportされているので、client側ではそれをimportします。
この時点でRPC機能は使えている状態です。
これが実体ではなく「型」であることがポイントで、それをhcという関数にジェネリクスで渡します。
これでクライアントオブジェクトができます。
APIリクエスト作成
/api/users
エンドポイントに、POSTリクエストを送信する処理を書きます。
レスポンスによってその後の表示を変えています。
clientの型がunknownになっている場合、server側の型定義に問題があるはずなので見直してください。
const client = hc<AppType>('/')
// この下から追記
// clientから.でチェインさせるだけで補完される。そのため、簡単にリクエストを作成できる
const res = await client.api.users.$post({
json: {
name: 'tarou',
age: 15,
},
})
if (res.ok) {
const user = await res.json()
console.log(res.status, user)
} else {
console.log(res.status, 'error')
}
clientサーバーも出来たので、bunを使って起動させてみます。
200 successと返ってくればOKです。
bun run client
$ bun run --hot src/client.ts
200 success {
name: "tarou",
age: 15,
}
この時点ではバリデーションがないので、jsonで適当な値を送っても200で返ってきます。
5. バリデーションとAPIドキュメントの自動生成
ここから少しserver.ts
の責務が増えますが、アーキテクチャの話はこの記事の要点ではないため、ファイル分割をしていません。
(自分がやるとしたら、schemaとroute、処理部分(handler)を別ファイルに分けたいという感じです。)
1. 事前準備
パッケージのインストール
@hono/zod-openapiをインストールします
Bun add @hono/zod-openapi
2. データの型を定義
データの型を定義することで、以下のメリットがあります。
- 入力データが正しい形式かチェックできる
- TypeScriptの型補完が効く
ここでは受け付けるリクエストの型、レスポンスの型を定義しています。
Zodでは、さまざまなデータの型を schema と呼んでいるようです。それに習いデータの型はxxxSchema
と命名しています。
Zod is a TypeScript-first schema declaration and validation library. I'm using the term "schema" to broadly refer to any data type, from a simple string to a complex nested object.
import { Hono } from 'hono'
import { z } from '@hono/zod-openapi' // @hono/zod-openapiをインポート
const app = new Hono()
// テスト用のデータ
const users = [
{id: 1, name: 'tarou', age: 15},
{id: 2, name: 'hanako', age: 20},
]
// ここから下にschemaを定義する記述追加
/**
* ユーザー作成時のリクエストボディ
*/
const reqCreateUserSchema = z
.object({
name: z.string().min(1),
age: z.number(),
})
/**
* エラーを返すスキーマ
*/
const resErrorSchema = z
.object({
code: z.number(),
message: z.string(),
});
/**
* ユーザー情報を返すスキーマ
*/
const resUserSchema = z.object({
id: z.number(),
name: z.string(),
age: z.number(),
})
3. データの型定義にドキュメント用の情報を追加する
先ほど作成したschemaに、openapi用の情報を追加します。
APIドキュメント生成時に使われます。
アプリケーションの大元オブジェクトを、OpenAPIに対応したものに置き換えます。
- import { Hono } from 'hono' // 置き換えるため、削除してOK
import { OpenAPIHono, z } from '@hono/zod-openapi'
const app = new OpenAPIHono() // API仕様書生成に対応した、オブジェクトを生成するように変更する
スキーマにOpenAPI用の情報を追加
* ユーザー作成時のリクエストボディ
*/
const reqCreateUserSchema = z
.object({
name: z
.string()
.min(1)
.openapi({
description: 'ユーザーの名前', // API仕様書に表示される説明を追加
example: 'tarou', // API仕様書に表示される例を追加
}),
age: z
.number()
.openapi({
description: 'ユーザーの年齢',
example: 15,
}),
}).openapi('reqCreateUserSchema') // 識別子を追加
createRouteを使ってルートを定義しなおす。
作成したスキーマを使ってAPIのルートを定義しなおします。
requestの内容、response内容を記述する事で、バリデーションが効くようになります。
リクエストハンドラーは.openapi
の第二引数にを書きます(リクエストがあった時に呼び出される関数)。
// サンプルルーティング
const sampleRoutes = app
// ここから下をまるごと書き換える
.openapi(
createRoute({
method: 'post',
path: '/api/users',
request: {
body: {
content: {
'application/json': {
schema: reqCreateUserSchema,
},
},
},
},
responses: { // 想定されるレスポンスはここで定義します
200: {
description: 'ユーザー情報を返す',
content: {
'application/json': {
schema: resUserSchema,
},
},
},
400: {
description: 'リクエストエラー',
content: {
'application/json': {
schema: resErrorSchema,
},
},
},
},
}),
async (c) => { // 第二引数にリクエストハンドラーを記述
// 事前に定義したスキーマに基づいてリクエストを検証、パスした場合のみデータを取得
const {name, age} = c.req.valid('json')
const user = {id: users.length + 1, name, age}
users.push(user)
return c.json(user, 200)
},
)
3. APIドキュメントの自動生成
すでに自動ドキュメント生成できる状態になっていますが、今のままだとJSON形式で出力されるため見づらいです。SwaggerUI
をつかって、見やすい形に整形しブラウザで確認できるようにします。
Hono用のものが用意されているので、これをインストールする
bun add @hono/swagger-ui
インポートして以下のように記述するだけで、自動生成されたAPIドキュメントをブラウザで確認できるようになります。
import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi'
import { swaggerUI } from '@hono/swagger-ui'
==========
// ドキュメントを生成
app.doc31("/doc", {
openapi: "3.1.0",
info: {
version: "1.0.0",
title: "Sample API Document",
},
});
// ドキュメントをブラウザで表示
app.get("/ui", swaggerUI({ url: "/doc" }));
export type AppType = typeof sampleRoutes
export default app
簡単な解説
.doc31 … OpenAPI 3.1バージョンのドキュメントを生成するためのHonoのメソッド
info部分は好きに変更することができます。
/doc
にGETリクエストすることでJSON形式でドキュメントを確認でき、/ui
だと整形されたAPIドキュメントがブラウザで確認できます。
6. テストコードの作成
Bunにはテストする機能も備わっているのでbun:test
からテストに必要なものをインポートできます。
HonoにもTestingヘルパーがあります。
testClientにアプリケーションのオブジェクトを渡すことで、型安全なテストを作成できます。
テスト用のファイルを追加
touch src/server.test.ts
簡単なテストを作成
import { describe, expect, it } from 'bun:test'
import { testClient } from 'hono/testing'
import app from './server'
import type { AppType } from './server'
describe('ユーザー系APIテスト', () => {
it('ユーザー作成が成功し200が返ってくる ', async () => {
const client = testClient<AppType>(app)
const res = await client.api.users.$post({
json: {
name: 'tarou',
age: 15,
},
})
expect(res.status).toBe(200)
})
it('ユーザー作成が失敗し400が返ってくる', async () => {
const client = testClient<AppType>(app)
const res = await client.api.users.$post({
json: {
// @ts-ignore 型チェックを無視, テストなので型チェックは外す
name: null,
age: 15,
},
})
expect(res.status).toBe(400)
})
})
npm scripts
を追加
"scripts": {
"dev": "bun run --hot src/server.ts",
"client": "bun run --hot src/client.ts",
"test": "bun test src/server.test.ts" ←追加
},
7. まとめと次のステップ
お疲れ様でした。
ここまで、お読みいただきありがとうございます!!
記事の内容はこれで終了となります。
この記事でやった内容まとめると、以下の内容になります。
Honoの基本的な使い方
- シンプルなAPIサーバーの構築方法
- RPC機能の実装方法
- Zodを使った型定義
- OpenAPI、SwaggerUIを使ったドキュメントの自動生成
- テストの書き方
ここから、次のステップも色々ありますが一例を書きました。
- データベースとの連携
- 認証機能の実装
- エラーハンドリング
- ディレクトリ構造の整理
- ミドルウェアの追加
- デプロイ
私も未熟な身であり、技術力の高い方からすると文章などに誤りがあるかと思いますので、コメントなどでご指摘いただけるとありがたいです。
この記事では基本的な機能に絞って解説しましたが、TypeScriptとHonoを使ったバックエンド開発に興味をお持ちの方の入門として参考になれば嬉しく思います。
参考記事
以下の記事を参考にさせていただきました!
Discussion