📝

HonoでAPIのドキュメントを自動生成する

2024/02/09に公開

はじめに

Honoでは@hono/zod-openapi@hono/swagger-uiを利用することで、APIのドキュメントを自動生成することができます。
今回は、この2つのパッケージを利用して、APIのドキュメントを自動生成する方法を紹介します。

Honoとは

Honoは軽量かつ、高速な事が特徴なWebフレームワークです。
さらに、特定の実行環境に依存しないように設計されているため、Node.jsやDeno、Cloudflare Workersなど、様々な環境で利用することができます。

また、Honoは様々なMiddleware[1]を提供しており、それらを組み合わせることで、様々な機能を実現することができます。
今回利用する@hono/zod-openapi@hono/swagger-uiもその一つです。

本題

下記のような受け取った入力をそのまま応答するAPIを題材に、APIのドキュメントを自動生成する方法を紹介します。

import { Hono } from 'hono';

const app = new Hono();

app.post('/echo', async context => {
  const body = await c.req.json()
  return context.json({ result: body.input });
});

export default app;

1. OpenAPIHonoインスタンスを作成する

通常Honoを利用する場合は、honoパッケージからHonoクラスをインポートし、インスタンスを作成します。
今回は@hono/zod-openapiを利用するため、@hono/zod-openapiパッケージからOpenAPIHonoクラスをインポートし、インスタンスを作成するように置き換えます。

import { OpenAPIHono } from '@hono/zod-openap';

const app = new OpenAPIHono();

app.post('/echo', async context => {
  const body = await c.req.json();
  return context.json({ result: body.input });
});

export default app;

2. OpenAPIのRoute定義を追加する

次に、@hono/zod-openapicreateRouteメソッドを利用して、OpenAPIのRoute定義を追加します。

import { createRoute, z } from '@hono/zod-openapi';

const route = createRoute({
  path: '/echo',
  method: 'post',
  description: '受け取った入力値をそのまま応答する',
  request: {
    body: {
      required: true,
      content: {
        'application/json': {
          schema: z.object({
            input: z.string().openapi({
              example: 'Hello World!',
              description: '入力',
            }),
          }),
        },
      },
    },
  },
  responses: {
    200: {
      description: 'OK',
      content: {
        'application/json': {
          schema: z.object({
            result: z.string().openapi({
              example: 'Hello World!',
              description: '応答',
            }),
          }),
        },
      },
    },
  },
});

3. Route定義を登録する

app.postメソッドの代わりにopenapiメソッドを利用し、先程作成したRoute定義を登録します。

app.openapi(route, async context => {
  const body = await c.req.json();
  return context.json({ result: body.input });
});

4. RequestBodyの取得方法を変更する

@hono/zod-openapiを利用する場合、context.req.validメソッドを利用してZodで検証済みの値を取得できます。
今回はRequestBodyを取得する為、jsonを指定していますが、他にもheaderqueryなども指定できます。

app.openapi(route, async context => {
  const body = await c.req.valid('json');
  return context.json({ result: body.input });
});

5. OpenAPIドキュメントを公開する

OpenAPIHonoインスタンスのdocメソッドを利用して、OpenAPIドキュメントを公開します。

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

6. SwaggerUIを公開する

最後に@hono/swagger-uiを利用することで、Swagger UIを公開することができます。
urlには、先程公開したOpenAPIドキュメントのURLを指定します。

import { swaggerUI } from '@hono/swagger-ui';

app
  .openapi(...)
  .doc('/specification', ...)
  .get('/doc', swaggerUI({
    url: '/specification',
  }));

以上で、APIのドキュメントをcreateRouteで定義した情報を元に自動生成されます。
サーバー起動後、/docにアクセスすることで、Swagger UIが表示され、APIのドキュメントを確認できます。

コード全文
import { swaggerUI } from '@hono/swagger-ui';
import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';

const app = new OpenAPIHono();

const route = createRoute({
  path: '/echo',
  method: 'post',
  description: '受け取った入力値をそのまま応答する',
  request: {
    body: {
      required: true,
      content: {
        'application/json': {
          schema: z.object({
            input: z.string().openapi({
              example: 'Hello World!',
              description: '入力',
            }),
          }),
        },
      },
    },
  },
  responses: {
    200: {
      description: 'OK',
      content: {
        'application/json': {
          schema: z.object({
            result: z.string().openapi({
              example: 'Hello World!',
              description: '応答',
            }),
          }),
        },
      },
    },
  },
});

app
  .openapi(route, async context => {
    const body = await c.req.valid('json');
    return context.json({ result: body.input });
  })
  .doc('/specification', {
    openapi: '3.0.0',
    info: {
      title: 'API',
      version: '1.0.0',
    },
  })
  .get('/doc', swaggerUI({
    url: '/specification',
  }));

export default app;

応用編 (Basic認証を掛ける)

業務で実際に利用する場合、APIのドキュメントは開発者のみが閲覧できるようにしたい場合があります。
そのような場合は、Basic認証を掛けることで、APIのドキュメントを閲覧できるユーザーを制限可能です。

1. SwaggerUIにBasic認証を掛ける

Honoには組み込みのMiddlewareから、Basic認証を掛けるためのbasicAuthMiddlewareが提供されています。
このbasicAuthMiddlewareを利用することで、簡単にBasic認証を掛けることができます。

import { basicAuth } from 'hono/basic-auth';

app
  .use('/doc', basicAuth({
    username: 'user',
    password: 'password',
  }));

app
  .openapi(...)
  .doc('/specification', ...)
  .get('/doc', ...);

2. OpenAPIドキュメントにBearer認証を掛ける

SwaggerUIへはBasic認証を掛けることが出来ましたが、このままではOpenAPIドキュメントには認証が掛かっていません。
OpenAPIドキュメントにも認証を掛けるために、同じく組み込みのMiddlewareから、Bearer認証を掛けるためのbearerAuthMiddlewareを利用します。

import { bearerAuth } from 'hono/bearer-auth';

app
  .use('/specification', bearerAuth({
    token: 'bearer-token',
  }))
  .use('/doc', ...);

app
  .openapi(...)
  .doc('/specification', ...)
  .get('/doc', ...);

3. SwaggerUIからOpenAPIドキュメントにアクセス出来るようにする

先程、OpenAPIドキュメントへBearer認証を掛けましたが、このままではSwaggerUIからOpenAPIドキュメントにアクセスすることができません。
そのため、SwaggerUIからOpenAPIドキュメントにアクセスできるようにするために、requestInterceptorを利用します。
requestInterceptorはSwaggerUIのリクエストを加工するための関数で、ここでBearerトークンをリクエストヘッダーに追加することで、SwaggerUIからOpenAPIドキュメントにアクセスできるようになります。

app
  .use('/specification', ...)
  .use('/doc', ...);

app
  .openapi(...)
  .doc('/specification', ...)
  .get('/doc', swaggerUI({
    url: '/specification',
    requestInterceptor: `
      request => {
        if (request.url === '/specification') {
          request.headers['authorization'] = \`Bearer bearer-token\`;
        }
        return request;
      }
    `,
  }));
コード全文
import { swaggerUI } from '@hono/swagger-ui';
import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
import { basicAuth } from 'hono/basic-auth';
import { bearerAuth } from 'hono/bearer-auth';

const app = new OpenAPIHono();

const route = createRoute({
  path: '/echo',
  method: 'post',
  description: '受け取った入力値をそのまま応答する',
  request: {
    body: {
      required: true,
      content: {
        'application/json': {
          schema: z.object({
            input: z.string().openapi({
              example: 'Hello World!',
              description: '入力',
            }),
          }),
        },
      },
    },
  },
  responses: {
    200: {
      description: 'OK',
      content: {
        'application/json': {
          schema: z.object({
            result: z.string().openapi({
              example: 'Hello World!',
              description: '応答',
            }),
          }),
        },
      },
    },
  },
});

app
  .use('/specification', bearerAuth({
    token: 'bearer-token',
  }))
  .use('/doc', basicAuth({
    username: 'user',
    password: 'password',
  }));

app
  .openapi(route, async context => {
    const body = await c.req.valid('json');
    return context.json({ result: body.input });
  })
  .doc('/specification', {
    openapi: '3.0.0',
    info: {
      title: 'API',
      version: '1.0.0',
    },
  })
  .get('/doc', swaggerUI({
    url: '/specification',
    requestInterceptor: `
      request => {
        if (request.url === '/specification') {
          request.headers['authorization'] = \`Bearer bearer-token\`;
        }
        return request;
      }
    `,
  }));

export default app;

まとめ

ただ軽量なだけでなく、様々なMiddlewareが提供されている為、実際の業務でも利用しやすいフレームワークであると思います。
シンプルなREST APIを構築する際には、Honoはベストな選択肢の一つであると言えるのではないでしょうか。
気になった方は、公式ドキュメントを参照して、実際に試してみてください!

ここまで読んでいただき、ありがとうございました。

脚注
  1. 外部ライブラリに依存しない組み込みのMiddlewareと、外部ライブラリに依存するサードパーティMiddlewareがあります。 ↩︎

PrAha

Discussion