3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Honoに入門してみる

Last updated at Posted at 2024-08-30

Honoとは

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

とりあえず実践

※ Cloudflare Workersで動かすことを前提とする。

  1. プロジェクトを作成する。

    • 以下のコマンドを実行する。
      npm create hono@latest my-app
      
    • いくつか質問されるので以下のように回答する。
      ? 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. 動作確認を行う。

JSONを返してみる

app.get('/api/hello', (c) => {
  return c.json({
    ok: true,
    message: 'Hello Hono!',
  })
})

リクエストをもう少し詳しく設定してみる

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処理を実装する。

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を実装してみる

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を実装してみる

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
Honoを使い倒したい2024
Honoを試す。良いWEBフレームワークですね。
Cloudflare Workersで使えるシンプルだけど奥が深いフレームワークHonoをためしてみた
Honoの概要とその特徴: Web標準に従った軽量高速フレームワーク

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?