Azure Functions + Hono でバックエンドAPIを作成する

## この記事の概要
普段はフロントエンジニア🐣の筆者が、訳あって**Azure Functions**を用いバックエンド実装をすることに。
**Hono**というフレームワークに出会い、色々調べながら

**1. Azure Functions(Static Web Apps付帯）上にHonoアプリを載せる**
**2. Swagger UIを作る**
**3. RPC機能を使ってフロントからのAPI通信を実装する**


……ところまでを実装し、「Honoすごい！！！！！」と賞賛するに至るまでを紹介する。


### 経緯
業務にて、Azure Static Web Apps付帯の**Azure Functions**を用いバックエンド実装をすることに。

フロントアプリはTypeScript + Reactで構築予定。

バックエンドは、他案件では大抵Python（Fast API）で構築していたが……
どうせならTypeScriptで作って、クライアント↔︎サーバー間で型の共有とかできるようにしたい。

（それに筆者Pythonの開発経験あんまりないし（小声））


ということで色々調べていたら、**Hono**というイケてるフレームワークを発見した……。


### Honoって？
https://hono.dev/

読み方は「ほの」ではなく「ほのお」。炎である🔥

HonoはCloudflare Workersに特化した「Webアプリを作るためのフレームワーク」として開発がスタート。
現在ではCloudflareのみならず、Fastly、Deno、Bun、AWS、Vercel、Node.jsなど、マルチに対応している。
（詳細は下記リンク先参照）

https://hono.dev/concepts/motivation

https://findy-code.io/engineer-lab/yusukebe


そして、Azure Functionsにも対応している……！

https://hono.dev/getting-started/azure-functions


Honoが持つさまざまな機能についての紹介は割愛するが、とにかくなんでもできる。

今回は**Zodを活用したSwagger UI生成機能**と**RPC機能**を盛り込みつつ、**Azure Functions上にHonoアプリを載せる**ところまでを実装する。

## 構成概要
### アプリ構成


|| クラウドリソース | フレームワーク |
|----| ---- | ---- |
|**フロントエンド**| Azure Static Web Apps | React + Vite + TypeScript |
|**バックエンド**| Azure Static Web Apps 付帯の **Azure Functions（v4）** | TypeScript + **Hono** |


### ディレクトリ構成概要
作成したディレクトリ構成はこちら（下記）

`api`ディレクトリ配下にAzure Functionsのソースを格納している。フロントアプリのソース内にAzure Functionsが同居しているイメージ。
この構成自体はAzure Static Web Apps公式Docsに倣っている。
（参考：[Azure Functions を使用して Azure Static Web Apps に API を追加する | Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/static-web-apps/add-api?tabs=vanilla-javascript#create-the-api)）


`api/app`ディレクトリでHonoアプリを構築し、`api/functions/httpTrigger.ts`でこのHonoアプリを呼び出している。

`api/app`配下はなるべく他案件のPythonアプリと同じディレクトリ構成にしている。
mainアプリでrouter各種をまとめ、routerでは各パス各HTTPメソッドごとに関数を設定、applicationsを呼んでロジック実行。DB接続系はrepositoriesにまとめ、必要に応じてapplicationsの関数から呼び出す。

```markdown
.
|-- dist/
|-- node_modules/ ………… フロントアプリのnode_modules
|-- public/
|     └-- staticwebapp.config.json
|-- src/ ………… フロントアプリのソース
|-- index.html
|-- package.json
|-- tsconfig.json
└── api/ ………… Azure Static Web Apps付帯のバックエンドAPI（Azure Functions）
     ├── .env   
     ├── dist  
     ├── host.json  
     ├── local.settings.json  
     ├── node_modules  
     ├── package.json  
     ├── tsconfig.json  
     └── src/
          ├-- app/ ………… Honoで構築したアプリ
          |    ├-- routes/ ………… ルーティング設定
          |    |      ├-- <domain名>/
          |    |      |     ├-- index.ts ………… ルーティング設定
          |    |      |     └-- honoClient.ts ………… フロントアプリ側でRPCモードで通信するためのクライアントオブジェクト
          |    |      └-- <domain名>/
          |    |      |     ├-- index.ts
          |    |      |     └-- honoClient.ts
          |    ├-- schemas/ ………… リクエスト/レスポンス型スキーマ定義
          |    |      ├-- <domain名>/
          |    |      |     └-- index.ts
          |    |      └-- <domain名>/
          |    |            └-- index.ts
          |    ├-- applications/ ………… 各ルートアクセス時のロジック
          |    |      ├-- <domain名>/
          |    |      |     └-- index.ts
          |    |      └-- <domain名>/
          |    |            └-- index.ts
          |    ├-- repositories/ ………… DB接続処理
          |    |      ├-- <domain名>/
          |    |      |     └-- index.ts
          |    |      └-- <domain名>/
          |    |            └-- index.ts
          |    └-- main.ts ………… /routes で定義したルーティング設定のグルーピング、Swagger UI生成
          |
          └-- functions/ ………… Azure Functions関数
               └-- httpTrigger.ts ………… path `/` へのHTTP Triggerで起動する関数定義
```


## 実装ポイント① httpTrigger.tsにHonoアプリを載せる
Azure Functionsは何らかのイベントをトリガーに発火する。今回はHTTPリクエストをトリガーに関数を呼び出す。

下記は[MS公式Docs](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cfunctionsv2&pivots=programming-language-typescript)記載のAzure Functions実装例。

``` ts
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';

export async function httpTrigger1(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    context.log(`Http function processed request for url "${request.url}"`);

    const name = request.query.get('name') || (await request.text()) || 'world';

    return { body: `Hello, ${name}!` };
}

app.http('httpTrigger1', {
    methods: ['GET', 'POST'],
    authLevel: 'anonymous',
    handler: httpTrigger1,
});
```

11行目以降の`app.http`でルーティング設定をしている。
関数名`httpTrigger1`というパスに`GET`・`POST`リクエストが来たら、関数`httpTrigger1`を実行する…………という内容になっている。

Azure Functionsだけでルーティング設定を行うのであればこの構成で良い。
が、今回ルーティング設定はAzure functionsではなくHono側で実行したい。
**どのパスにどのHTTPメソッドのリクエストが飛んだとて、Honoのアプリをトリガーするようにしたい！！！**

ということで、以下のように実装した。
``` api/src/functions/httpTrigger.ts
import { app } from "@azure/functions";
import { azureHonoHandler } from "@marplex/hono-azurefunc-adapter";
import honoApp from "../apps/main";

app.http("httpTrigger", {
  methods: [
    // Add all your supported HTTP methods here
    "GET",
    "POST",
    "DELETE",
    "PUT",
  ],
  authLevel: "anonymous",
  route: "{*proxy}",
  handler: azureHonoHandler(honoApp.fetch),
});

```

注目して欲しいのは`app.http`の第二引数`options`オブジェクト。

ここの`route`設定は、「関数がどの要求 URL に応答するかを制御するルート テンプレートを定義」する（[参照：Azure Functions の HTTP トリガー > 構成](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cfunctionsv2&pivots=programming-language-typescript#configuration)）。

ここに`{*proxy}`という値をセットした。

<br/>

中かっこ`{}`で囲まれた文字列はパスパラメータ名を示す。たとえば`/pets/{petId}`とした場合、`/pets/cat01`とかがマッチする。

さらに、`/pets/{*petId}`のようにワイルドカード付きにすると、リクエストURLの残りのパスセグメントの文字列表現になる。`/pets/cat01`でも`/pets/cat/01`でも`/pets/cat/mike/01`でもマッチする、というわけだ。
（参考：[レガシ Azure Functions プロキシを使用する | Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/azure-functions/legacy-proxies#route-template-parameters)）

<br/>

ということは、`{*proxy}`は、`/book`にも`/book/list`にも`/book/info/hoge`にもマッチする。
これを`app.http`の第二引数`options`の`route`設定で指定したことで、どんなパスへのHTTPリクエストであろうと本Functionsを呼び出すことができるようになった。

やったね！！！

<br/>

そして、`handler`設定では [HonoのAzure Functions Adapter](https://github.com/Marplex/hono-azurefunc-adapter)を使ってHonoアプリを載せている。
詳細は下記参照。

https://hono.dev/getting-started/azure-functions

これだけでHonoアプリをAzure Functionsに載せることができた…………！

## 実装ポイント② Swagger UI生成
FastAPIといえば自動ドキュメント生成機能が大きなメリットの一つ（だと思っている）。
他案件ではFastAPIを活用し、Swaggerを見ながら仕様理解を深めている弊組織（わかりやすくていいよね）。
ということで、今回「Honoに乗り換えたからSwagger作れなかったんですよね🤪」は絶対に許されない。

でもその辺もHonoはしっかりサポートしていた…………。

https://hono.dev/snippets/zod-openapi

今回はこちらのHonoミドルウェア**zod-openapi**を用いて実装。

<br/>

実装例はこちら。

`api/apps/router/book/index.ts`にて、zod-openapiを用いてHonoApp`bookApp`を生成。書籍一覧を取得するAPIを`bookApp`に追加し、エクスポートしている。

APIのリクエスト・レスポンス定義ではZodのスキーマ定義を用いるが、このスキーマ内で細かい値の情報（項目ごとの説明文やvalueの例など）を記載すると、Swagger UIにも反映される。

```api/src/apps/schemas/book/index.ts
import { z } from "@hono/zod-openapi";

/** book情報 ベース - スキーマ定義 */
const bookInfoBaseSchema = z.object({
  id: z.string().openapi({
    example: "71b37846-e855-4736-97f3-f8d730b92208",
    description: "Book ID",
  }),
  createdAt: z.string(),
  updatedAt: z.string(),
  label: z.string().openapi({
    example: "テストBook1", // `label`の参考value
    description: "本のタイトル", // `label`の説明文
  }),
  description: z.string().nullable().openapi({
    example: "本の説明文です。",
    description: "本の説明",
  }),
});

/**
 * ***********************
 * GET /api/book/{id}
 * ***********************
 */

/** GET /api/book/{id} リクエスト - スキーマ定義 */
export const getBookByIdRequestSchema = z.object({
  id: z.string().openapi({
    param: { // パスパラメータであることを示す
      name: "id",
      in: "path",
    },
    example: "book1",
  }),
});
/** GET /api/book/{id} リクエスト - 型定義 */
export type GetBookByIdRequest = z.infer<typeof getBookByIdRequestSchema>;

/** GET /api/book/{id} レスポンス - スキーマ定義 */
export const getBookByIdResponseSchema = z.object({
  result: bookInfoBaseSchema,
});
/** GET /api/book/{id} レスポンス - 型定義 */
export type GetBookByIdResponse = z.infer<typeof getBookByIdResponseSchema>;

```


```api/src/apps/router/book/index.ts
import { OpenAPIHono, createRoute } from "@hono/zod-openapi";
import {
  getBookByIdRequestSchema,
  getBookByIdResponseSchema,
} from "../../schemas/book";
import * as application from "../../applications/book";

const bookApp = new OpenAPIHono();

// 〜〜〜中略〜〜〜

/** GET /api/book/{id} */
export const getBookByIdRoute = bookApp.openapi(
  createRoute({
    method: "get",
    path: "/api/book/{id}",
    description: "Get book by ID", // SwaggerUIに表示するAPI説明文
    tags: ["book"], // SwaggerUIのタグ
    request: {
      params: getBookByIdRequestSchema,
    },
    responses: {
      200: {
        content: {
          "application/json": {
            schema: getBookByIdResponseSchema,
          },
        },
        description: "book info",
      },
    },
  }),
  async (c) => {
    const id = c.req.param("id");
    const res = await application.getBookById(id!);
    return c.json(res, 200);
  },
);


// 〜〜〜中略〜〜〜

export { bookApp };
```


そして、`bookApp`のようなルーター各種を束ねているのが`api/apps/main.ts`。
ここでHonoApp`app`を生成し、ルーター各種を取り込んでいる。

さらに、`app`に対しSwagger UIの公開設定を行っている。

1. `/api/openapi`でAPI仕様のオブジェクトを公開
2. `/api/docs`へのGETリクエストで、`/api/openapi`で公開している情報をSwagger UIにし生成

……という流れである。


```api/src/apps/main.ts
import { logger } from "hono/logger";
import { OpenAPIHono } from "@hono/zod-openapi";
import { swaggerUI } from "@hono/swagger-ui";
import { bookApp } from "./router/book";

const app = new OpenAPIHono()
  // Swagger UI
  .doc("/api/openapi", { // このパスにアクセスするとOpenAPIで生成されたAPI仕様JSONが表示される
    info: {
      title: "An API",
      version: "v1",
    },
    openapi: "3.1.0",
  })
  .get(
    "/api/docs", // このパスにアクセスすると、API仕様JSONをSwagger UIに変換したものが表示される
    swaggerUI({
      url: "/api/openapi",
    }),
  );

app.use("*", logger());

app.route("/", bookApp);

export default app;

```

さて、実際に`api/docs/`にアクセスして確認してみよう。
ローカルサーバー`http://localhost:7071/api/docs`にアクセスすると……。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/3c9e9d56-87c6-b1df-398c-24db6b92cc4d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/8ba9802c-bb18-8f17-a4ff-b6790b8e90c7.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/1b8ac2a8-e7b2-e6d8-b71c-8533bcea27ad.png)

ちゃんとAPI仕様が公開されている！！！
やったねできたね！！！


## 実装ポイント③ RPC機能

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

https://zenn.dev/yusukebe/articles/a00721f8b3b92e


RPC機能を使うと、サーバとクライアントでAPI仕様を共有でき、型安全かつ高速にクライアントとサーバーの通信を実装できる。

類似ツールにtRPCやts-restなどがあるが、HonoにはデフォルトでRPC機能がついているので個別パッケージをインストールする必要はない。またREST APIベースでかなり簡単に実装できる。

<br/>

実装例は以下。

まず、`hc`という関数に、フェッチしたいAPIルーターの型をジェネリクスとして渡し、クライアントオブジェクトを作成する。

そして、API接続したい箇所でこのクライアントオブジェクトを呼び出す。

<br/>
構成は色々あると思うが、今回は、

1. サーバー側の`api/src/apps/router/<domain名>/honoClient.ts`で各APIのクライアントオブジェクトを生成する関数をエクスポートする
2. クライアント側でサーバーのエンドポイントURLを設定し、クライアントオブジェクトを生成する

という具合にしている。
クライアント側で`hc`とフェッチしたいルーターをそれぞれインポートするのがちと面倒だったので、サーバ側でクライアントオブジェクトを生成→エクスポートした、というわけだ（ものぐさ）。


``` api/src/apps/router/book/honoClient.ts
import { hc } from "hono/client";
import { deleteBookInfoRoute, getBookByIdRoute, getBookListRoute, postBookInfoRoute, putBookInfoRoute } from ".";

/** GET /api/book/list - クライアントオブジェクト生成関数 */
export const getBookListRouteClient = (url: string) => hc<typeof getBookListRoute>(url);

/** GET /api/book/{id} - クライアントオブジェクト生成関数 */
export const getBookByIdRouteClient = (url: string) => hc<typeof getBookByIdRoute>(url);
```

```クライアント側のファイル.tsx
import { Table, TableBody, TableCell, TableHead, TableRow } from "@mui/material";
import Typography from "@mui/material/Typography";
import { useSuspenseQuery } from "@tanstack/react-query";
import { getBookListRouteClient } from "api/src/apps/router/book/honoClient";
import { useEffect } from "react";

export const Top = () => {
  /** GET /api/book/list API呼び出し */
  const getBookList = async () => {
    // hono client生成
    const client = getBookListRouteClient("http://localhost:7071");
    const res = await client.api.book.list.$get();
    return res.json();
  };

  // API呼び出し
  useEffect(() => {
    const fc = async () => {
      const res = await getBookList();
      console.log("[res]", res);
    };
    fc();
  }, []);

  /** API呼び出し(Tanstack Query使用） */
  const { data } = useSuspenseQuery({
    queryKey: ["getBookList"],
    queryFn: getBookList,
  });

  return (
    <>
      <Typography variant="h6">トップページ</Typography>
      <Typography variant="body1">APIテスト</Typography>
      {/* APIテスト結果 */}
      <Table sx={{ width: "500px" }}>
        <TableHead>
          <TableRow>
            <TableCell>ID</TableCell>
            <TableCell>名称</TableCell>
          </TableRow>
        </TableHead>
        {data?.map((item) => (
          <TableBody key={item.id}>
            <TableCell>{item.id}</TableCell>
            <TableCell>{item.label}</TableCell>
          </TableBody>
        ))}
      </Table>
    </>
  );
};


```

初見だととっつきづらいのは`const res = await client.api.book.list.$get();`の部分かと思う。これは、APIパスをピリオド区切りにして、`$`以下でHTTPメソッドを指定しているイメージ。
（上記例は `GET /api/book/list` へのリクエスト）
実は**エディタ上で自動で補完してくれる**ので、脳死でも書けてしまう🌸

やっていることとしては、各APIに対応したクライアントオブジェクトに、リクエスト情報を都度セットしてあげているだけ。慣れてしまえばどうってことない。

そして何より、**リクエスト・レスポンス部分も型定義を補完してくれている**。`res`にカーソルを当てればこんな感じで型定義を出してくれる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/5e069039-f99e-d76d-9829-1d1118ccf889.png)


今までの開発だと、APIリクエスト/レスポンスの型/スキーマ定義を、フロント側でも個別定義していた。
型の共有ができないので仕方ないとはいえ、両方の定義をきちんと合わせるのが完全手作業で、ヒューマンエラーが起こりがちだった。何よりクソ面倒だった。

それがどうだ、このRPC機能を使えば、
**API接続用のオブジェクトをサーバー側で生成しているので、サーバー側のスキーマ定義をクライアント側でも利用できている、それも自動で。**

しかも、**APIパスもクライアントオブジェクトに含まれているので、パス指定時でミスることもない。** エディタが補完してくれるし、万が一typoしてもエディタが怒ってくれるからめちゃくちゃ安全。

フロント側で個別リクエスト/レスポンスのスキーマ/型定義を行う必要がないから、記述量が格段に減少。
API側の型定義が変わったからフロント側の型定義も変える……みたいな煩雑な対応が不要になり、メンテコストも低減！

爆速で型安全なAPI通信を実装できた…………！


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/16046f40-f6d3-045e-fea7-9a5c455ef840.png)
ちゃんと通信できてるね！


## 終わりに
無事にAzure Functions + Hono でバックエンドAPIを構築することに成功。
FastAPIで実現できていたドキュメント自動生成機能も担保しつつ、RPC機能等で開発体験も爆上がりした。
他メンバーからも「便利でいい（特にRPC機能）」と好評。

筆者が普段はフロントメインで対応していることもあり、今回はフロント開発体験に大きく寄与しそうな部分だけを取り上げたが、
Honoには他にも面白そうな機能がある＆日々アップデートされているので、今後も動向を追いかけつつ色々取り入れてみたい…………。