🔥

TypeScript初心者の私がHonoでバックエンドサーバー構築してみた ~RPCからテストまで~

2025/02/10に公開

TypeScriptとHonoで作る最小構成のAPIサーバー

はじめに

私は、フロントエンドエンジニアのゆず(@yuzunosk55)です。

この記事では、TypeScriptを使ってAPIサーバーを構築する方法を解説します。
フレームワークには、最近注目を集めているHonoを使用します。

主な内容:

  1. APIサーバーの基本的な構築方法
  2. Honoの特徴的な機能であるRPCの実装
  3. 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という名前になったようです。

https://hono.dev/

なぜExpressではなくHonoを選ぶのか?

私の経験からExpressとHonoを比較してみたいと思います!

Expressの特徴

Expressは非常にミニマルなフレームワークです。
最初にExpressのドキュメントを見たときは驚きました。
本当にフレームワークなのかと思うほど、公式ドキュメントが薄いのです。

理由は、Expressの設計思想にあります:

  • シンプルなフレームワーク
  • 必要な機能は全てプラグインとして追加する
  • 開発者が自由に設計できる

(余談:これほどにシンプルで小さいのは、RubyのフレームワークSinatoraに影響を受けているからだとWikiに記述されていました。https://ja.wikipedia.org/wiki/Express.js)

https://expressjs.com/ja/

この自由度の高さは、諸刃の剣のように感じました。

例えるなら…
「広大な平原に降ろされて、『はい、家を建ててください!』と言われているような感じ」です。
経験や知識がないと、どこから始めれば良いのか途方に暮れてしまいます。

Honoの魅力

そんな時に出会ったのがHonoです!
Honoの公式サイトには、こんな言葉が書かれていました。

Batteries Included
Hono has built-in middleware, custom middleware, third-party middleware, and helpers. Batteries included.

これは重要なポイントで:

  1. 必要な機能が最初から揃っている
  2. 追加のインストールが少なくて済む
  3. 公式ドキュメントを読めば、構築に必要な情報が手に入る

さらに、Honoには以下のような利点もあります:

  • TypeScriptとの相性が抜群
  • RPCによる型安全な通信が可能
  • 基本的な書き方がExpressとほぼ同じなので、学習コストが下がる。
  • Honoに機能が内包されていることで、package.jsonの記述量が減る。

これらの利点に魅力を感じ、私はHonoを学び始めました。

その他、構築に使われている技術の簡易説明

Bunの特徴と利点

Node.jsの代替となる新しい実行環境です。実行環境としてだけでなく色んなことができます。
とにかく速い!!まだすべてのnpmパッケージに対応できてないようですが、速すぎて開発体験爆上りします。

  • Node.jsの代替となる高速な JavaScript/TypeScript実行環境。
  • パッケージマネージャーとしても使える(npm/yarnの代替)。超早い。
  • テストも出来る。

https://bun.sh/

Zodで型安全性が確保できる

TypeScript向けのスキーマ検証/バリデーションに使えるライブラリです。

  • TypeScriptの型が自動生成できる。
  • 直感的に書ける(例:z.string()、z.number())。

https://zod.dev/

OpenAPIとSwagger UIの役割

APIの仕様書を自動生成するのに使うツールです。

OpenAPI

  • APIの仕様を記述するための標準規格。
  • これを使ってコーディングしていくだけで、コードがドキュメントを自動生成してくれる。
  • API修正時に、ドキュメントを修正する手間がなくなる。

Honoには、ZodとOpenAPIを使うためのパッケージも用意されている。
https://github.com/honojs/middleware/tree/main/packages/zod-openapi

SwaggerUI

  • APIドキュメントをブラウザで確認できるツール。

Honoには、SwaggerUIを使うためのパッケージも用意されている。
https://github.com/honojs/middleware/tree/main/packages/swagger-ui

HonoのRPC機能

RPCを使うと、以下のような利点があります。

  • 型安全性向上
    • APIのリクエスト/レスポンスの型チェック
    • 入力補完による開発効率の向上
    • APIの呼び出しがシンプルになる。

https://www.ntt-west.co.jp/business/glossary/words-00229.html

https://hono.dev/docs/guides/rpc

型情報がある場合とない場合の比較イメージ
型情報がない場合

// サーバー側
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を使ったセットアップ方法も公式に用意されているので、基本はこれ通りに進めれば大丈夫です。
https://hono.dev/docs/getting-started/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.jsonnpm scriptsに記述されているdevコマンドで、サーバーをホットリロード付きで起動させてくれています。そのため、ファイルの変更が即座に反映されます。

5. ts-configを修正

Bunの公式にTypeScriptおすすめ設定があるので、これを使います。
(config設定が理解できている方は、自由に設定してください。)
@types/bunをインストールするよう書かれていますが、既にインストールされているので、スルーして大丈夫です。

https://bun.sh/docs/typescript#suggested-compileroptions

この段階で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.tsclient.tsを用意します。

mv src/index.ts src/server.ts # リネーム
touch src/client.ts 
ls src/
client.ts  server.ts

package.jsonの更新

実行コマンドを更新します。

package.jsonnpm scriptsを更新します。

package.json
  "scripts": {
    "dev": "bun run --hot src/server.ts",
    "client": "bun run --hot src/client.mts",
  },

2. 超簡単なAPI作成

RPCを実装したいだけなので、最小で作ります。
※ この記事ではDB連携はしません。

ここで記述するコードの全体は以下になります。

src/server.ts
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アプリケーションオブジェクトを作成しています。

src/server.ts
const app = new Hono()

作成したHonoアプリケーションオブジェクトに対して.HTTP_METHODと記述することで、各メソッド、各パスでリクエストを受け付けられます。
下記の記述の場合/api/usersというエンドポイントに、POSTメソッドでリクエストを受け付けるAPIを作成しています。

リクエスト時に渡されてきたデータはc(context)の中に入っています。
c.req.json()と記述することでリクエストボディのデータをJavaScriptのオブジェクトにパースして取得します。

appメソッド
https://hono.dev/docs/api/hono#methods

contextについて
https://hono.dev/docs/api/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.tsexport default appしてる行の上で、クライアント側で型情報を取得できるようexportします。

src/server.ts

//型を情報を出力したいルートを変数にいれる
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を使う方法として記述されています。
https://hono.dev/docs/guides/best-practices#if-you-want-to-use-rpc-features

typeofについて
https://typescriptbook.jp/reference/type-reuse/typeof-type-operator

※ 大規模なアプリケーションを構築する場合、下記を参考にしてください。
それぞれのエンドポイントでチェインさせたルートをexportすれば、RPC機能がつかえるはずです。
https://hono.dev/docs/guides/best-practices#building-a-larger-application

記述例

エンドポイントごとにファイルを分割

src/server/routes/users.ts
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

親ファイルで読み込ませる

src/server/index.ts
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を呼び出すクライアントを最低限のコードで作成します。

クライアントオブジェクト作成

まずは以下の記述を書いてください。

src/client.ts
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側の型定義に問題があるはずなので見直してください。

src/client.ts
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.

src/server.ts
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に対応したものに置き換えます。

src/server.ts
- import { Hono } from 'hono' // 置き換えるため、削除してOK
import { OpenAPIHono, z } from '@hono/zod-openapi'

const app = new OpenAPIHono() // API仕様書生成に対応した、オブジェクトを生成するように変更する

スキーマにOpenAPI用の情報を追加

src/server.ts/**
 * ユーザー作成時のリクエストボディ
 */
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の第二引数にを書きます(リクエストがあった時に呼び出される関数)。

src/server.ts
// サンプルルーティング
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ドキュメントをブラウザで確認できるようになります。

src/server.ts
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

簡単なテストを作成

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を使ったバックエンド開発に興味をお持ちの方の入門として参考になれば嬉しく思います。

参考記事

以下の記事を参考にさせていただきました!

https://zenn.dev/yusukebe/articles/a00721f8b3b92e#普通のrest-apiを書く

https://tech.fusic.co.jp/posts/hono-zod-openapi/#zodスキーマ

https://zenn.dev/slowhand/articles/b7872e09b84e15#必要なパッケージ追加

https://zenn.dev/masahiro_dev/articles/zod-cheat-sheet#1.-primitive

Discussion