Honoに入門してみる

# Honoとは
Hono (日本語の炎🔥からきてるみたい) は、Web標準に基づいて構築された小型でシンプル、かつ超高速なWeb フレームワークのこと。
Cloudflare Workers、Fastly Compute、Deno、Bun、Vercel、Netlify、AWS Lambda、Lambda@Edge、Node.js など、あらゆるJavaScriptランタイムで動作する。

# とりあえず実践
※ Cloudflare Workersで動かすことを前提とする。

1. プロジェクトを作成する。
    - 以下のコマンドを実行する。
        ```bash
        npm create hono@latest my-app
        ```
    - いくつか質問されるので以下のように回答する。
        ```bash
        ? Which template do you want to use?
        #=> cloudflare-workers

        ? Do you want to install project dependencies?
        #=> yes

        ? Which package manager do you want to use?
        #=> npm
        ```

2. 依存関係をインストールする。
    - 以下のコマンドを実行する。
        ```
        cd my-app
        npm i
        ```

3. ファイルが作成されているか確認する。
    - ss
        ```src/index.ts
        import { Hono } from 'hono'
        
        const app = new Hono()

        // cはcontextの略
        app.get('/', (c) => {
          return c.text('Hello Hono!')
        })
        
        export default app
        ```

4. 動作確認を行う。
    - 以下のコマンドを実行する。
        ```
        npm run dev
        ```
    - 開発サーバーを起動後、http://localhost:8787ブラウザでアクセス。

## JSONを返してみる
```ts
app.get('/api/hello', (c) => {
  return c.json({
    ok: true,
    message: 'Hello Hono!',
  })
})
```

## リクエストをもう少し詳しく設定してみる
```ts
app.get('/posts/:id', (c) => {
  // クエリパラメーターpageを取得
  const page = c.req.query('page')

  // パスパラメーターidを取得
  const id = c.req.param('id')

  // ヘッダーを設定
  c.header('X-Message', 'Hi!')
  
  return c.text(`You want see ${page} of ${id}`)
})
```

## 軽くスキーマ駆動開発してみる

まずはスキーマを定義する。
```src/schema/users.schema.ts
import { z } from '@hono/zod-openapi'

export const ParamsSchema = z.object({
  id: z
    .string()
    .min(3)
    .openapi({
      param: {
        name: 'id',
        in: 'path',
      },
      example: '1212121',
    }),
})

export const UserSchema = z
  .object({
    id: z.string().openapi({
      example: '123',
    }),
    name: z.string().openapi({
      example: 'John Doe',
    }),
    age: z.number().openapi({
      example: 42,
    }),
  })
  .openapi('User')
```

APIのルートを定義する。

```src/routes/users.route.ts
import { createRoute } from '@hono/zod-openapi';
import { ParamsSchema, UserSchema } from '../schema/users.schema';

export const userShowRoute = createRoute({
  method: 'get',
  path: '/users/{id}',
  request: {
    params: ParamsSchema,
  },
  responses: {
    200: {
      content: {
        'application/json': {
          schema: UserSchema,
        },
      },
      description: 'Retrieve the user',
    },
  },
})
```

最後にAPI処理を実装する。

```ts
import { OpenAPIHono } from '@hono/zod-openapi'
import { userShowRoute } from '../routes/users.route';

const app = new OpenAPIHono()

app.openapi(userShowRoute, (c) => {
  const { id } = c.req.valid('param')

  return c.json({
    id,
    age: 20,
    name: 'Ultra-man',
  })
})

app.doc('/doc', {
  openapi: '3.0.0',
  info: {
    version: '1.0.0',
    title: 'My API',
  },
})
```

## Cloudflare Durable Objectsを実装してみる

```ts
import { Hono } from 'hono'

export class Counter {
  value: number = 0
  state: DurableObjectState
  app: Hono = new Hono()

  constructor(state: DurableObjectState) {
    // Constructorに渡されたDurableObjectStateを保存
    this.state = state
    // 複数のリクエストが同時に実行されるのを防ぐため、stateに保存された値を取得する際にブロックを行う
    this.state.blockConcurrencyWhile(async () => {
      const stored = await this.state.storage?.get<number>('value') // ストレージからvalueを取得
      this.value = stored || 0 // 値が存在すればそれを使用し、存在しなければ0に初期化
    })

    // '/increment'エンドポイントにアクセスした際、カウンターの値をインクリメント
    this.app.get('/increment', async (c) => {
      const currentValue = ++this.value // カウンターの値をインクリメント

      await this.state.storage?.put('value', this.value) // 新しい値をストレージに保存

      return c.text(currentValue.toString()) // インクリメントされた値を文字列としてレスポンス
    })

    // '/decrement'エンドポイントにアクセスした際、カウンターの値をデクリメント
    this.app.get('/decrement', async (c) => {
      const currentValue = --this.value // カウンターの値をデクリメント

      await this.state.storage?.put('value', this.value) // 新しい値をストレージに保存

      return c.text(currentValue.toString()) // デクリメントされた値を文字列としてレスポンス
    })

    // '/'エンドポイントにアクセスした際、現在のカウンターの値を返す
    this.app.get('/', async (c) => {
      return c.text(this.value.toString()) // 現在の値を文字列としてレスポンス
    })
  }

  // 受け取ったリクエストをHonoアプリケーションに渡して処理する
  async fetch(request: Request) {
    return this.app.fetch(request)
  }
}
```

## Cloudflare Queuesを実装してみる

```ts
import { Hono } from 'hono'

type Environment = {
  readonly ERROR_QUEUE: Queue<Error> // エラーを保存するためのキュー
  readonly ERROR_BUCKET: R2Bucket // エラーログを保存するためのバケット
}

const app = new Hono<{
  Bindings: Environment // Bindingsとして先に定義したEnvironment型を指定
}>()

// ルートパス '/' に対するGETリクエストのハンドラーを定義します
// 今回はリクエストが成功するか失敗するかをランダムに決定する処理を実装
app.get('/', (c) => {
  if (Math.random() < 0.5) {
    return c.text('Success!')
  }
  throw new Error('Failed!')
})

// グローバルなエラーハンドラーを定義
// リクエストが失敗した場合にエラーをキャッチし、エラーをキューに送信
// エラーメッセージを含む500ステータスのレスポンスを返す
app.onError(async (err, c) => {
  await c.env.ERROR_QUEUE.send(err) // エラーをキューに送信

  return c.text(err.message, { status: 500 })
})

export default {
  fetch: app.fetch,
  
  // キューにバッチとしてエラーが送られてきたときに処理するためのハンドラーを定義します。
  async queue(batch: MessageBatch<Error>, env: Environment) {
    let file = ''

    // バッチ内のすべてのメッセージ（エラー）を処理し、それらのスタックトレースやメッセージを連結して一つのファイルとして保存します。
    for (const message of batch.messages) {
      const error = message.body
      file += error.stack || error.message || String(error)
      file += '\r\n' // エラーの各エントリーを改行で区切る
    }

    // バケットにエラーファイルを保存します。ファイル名は現在のタイムスタンプに基づいています。
    await env.ERROR_BUCKET.put(`errors/${Date.now()}.log`, file)
  },
}
```


# 参考
[Hono](https://hono.dev/docs/)
[Honoを使い倒したい2024](https://zenn.dev/aishift/articles/a3dc8dcaac6bfa)
[Honoを試す。良いWEBフレームワークですね。](https://qiita.com/bee-yan/items/47236e439d8c2fa18408)
[Cloudflare Workersで使えるシンプルだけど奥が深いフレームワークHonoをためしてみた](https://dev.classmethod.jp/articles/hono-on-cloudflare-workers/)
[Honoの概要とその特徴: Web標準に従った軽量高速フレームワーク](https://www.issoh.co.jp/column/details/3001/)