見出し画像

HonoのRPC機能を使いつつ、レポジトリを分割する方法を検討してみた


動機

SHIFTのKoshiishiです。

現在、あるアプリケーションがバックエンド: Express、フロントエンド: Vue という構成で動いており、モノレポで管理されています。
モノレポ自体は問題ではないのですが、このアプリケーションが担う責務が複数にわたり、アプリケーションのアプリケーションのような構成になってしまっています。

また基本的にJavaScriptアプリケーションであるため、型ファイルを手書きして部分的に補完を利かせているもののそのファイルの更新コストが大きいという問題もあります。

そこで、大きく以下の方向性で技術スタックの刷新を検討しています:

  1. バックエンドを Express → Hono に置き換え

  2. フロントエンドを Vue → React に置き換え

  3. モノレポからリポジトリ分割へ移行し

    1. バックエンドは 1 つのリポジトリで管理 (BackendRepo)

    2. フロントエンドは複数リポジトリに分割 (FrontendRepoX, FrontendRepoY, ...)

分割することで、チーム構成やプロダクト特性に応じてリポジトリ単位の責任範囲をはっきりさせたいと考えています。また、バックエンドのAPIの型をフロントエンドに渡すことで、開発生産性の向上を期待しています。
現状厚めのテストでカバーしている範囲の一部を、徐々に静的な検査に寄せていく狙いがあります。

静的解析でカバーできる品質の部分とテストでカバーすべき部分は完全に置き換え可能なものではありませんが、静的検査を活用してバリデーションの実装を強固にし、よりドメインに集中したテストに注力していきたいです。

この記事でやってみたこと


簡素なTodoアプリのバックエンドをHonoで作成し、GitHub PackagesのnpmレジストリにAPIの型を登録することでHonoのRPC機能を使いつつも、レポジトリが分割可能であることを確かめてみました。
最終的なコードはこちらにあります。
userとtodoのテーブルがあり、作成と一覧APIのみがあります。

Honoプロジェクトの作成

まずはバックエンド側のプロジェクトを Hono + TypeScript で新規作成します。 ここでは Bun を利用してプロジェクト生成から開発を行います。

❯ bun create hono@latest hono-todo-backend
create-hono version 0.14.3
✔ Using target directory … hono-todo-backend
? Which template do you want to use? nodejs
? 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 hono-todo-backend

上記で hono-todo-backend ディレクトリが生成され、簡単なサンプルの Hono アプリが用意されます。
この段階で、cd hono-todo-backend して Bun 開発サーバを起動すれば動作確認が可能です。

注意: 今回は手軽さを優先して root ユーザーで作業し、.env と docker-compose.yml を用意して進めています。またパスワードなども文字列でベタ書きしている場面がありますが、実際の運用ではよりセキュアな管理を行ってください。

Docker と Prisma のセットアップ

Docker コンテナで DB を立ち上げる

docker-compose.yml を用意します

services:
  db:
    image: mysql:8.0
    container_name: todo-mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: todo_db
    ports:
      - "3306:3306"
    command: --default-authentication-plugin=mysql_native_password

その後、 docker compose up -d でデータベースコンテナを起動します。

Prisma の導入

次に、Hono プロジェクト内で ORM として Prisma を利用するために、以下のコマンドを実行します。

bun add prisma @prisma/client
❯ bun x prisma init

prisma init を実行すると、プロジェクト内に prisma/ ディレクトリと prisma/schema.prisma、および .env ファイルが生成されます。 .env にはデータベース接続情報 (DATABASE_URL) を記載します。

Prisma スキーマの編集

prisma/schema.prisma が生成されますが。
今回previewFeaturesの一つであるprismaSchemaFolderをオンにして、prisma/フォルダ以下にprismaファイルを分けてみました。
参考: How to enable multi-file Prisma schema support schema.prismaに設定を、todo.prisma,user.prismaではそれぞれ定義を行いました。
詳細はこちら

❯ tree prisma/
prisma/
└── schema
    ├── schema.prisma
    ├── todo.prisma
    └── user.prisma

マイグレーション実行

❯ bun x prisma migrate dev --name init

これで初回のマイグレーションが走り、DBにテーブルが作成されます。

❯ bun x prisma generate

prisma clientを生成します

APIの実装と型のexport

(本来はzodによるバリデーション等行うべきですが、今回は動作の検証が目的なので雑なバリデーションにしています)
TodoアプリのAPIの一部として、ユーザー, Todoの追加と一覧だけを雑に実装します
Todoの追加と一覧

export const todoApiRoutes = app.get('/todo', async (c) => {
  const todos = await prisma.todo.findMany()
  return c.json(todos)
}).post('/todo', async (c) => {
  const body = await c.req.json()
  const { title, userId } = body
  if (!title || !userId) {
    return c.json({ error: 'title and userId are required' }, 400)
  }

  const newTodo = await prisma.todo.create({
    data: {
      title: title as string,
      userId: Number(userId),
    },
  })
  return c.json(newTodo, 201)
})

userの追加と一覧

export const userApiRoutes = app.post('/user', async (c) => {
  const body = await c.req.json()
  const { name, email, password } = body
  if (!name || !email || !password) {
    return c.json({ error: 'name, email, password are required' }, 400)
  }

  const newUser = await prisma.user.create({
    data: {
      name: name as string,
      email: email as string,
      password: password as string,
    },
  })
  return c.json(newUser, 201)
}).get('/user', async (c) => {
  const users = await prisma.user.findMany()
  return c.json(users)
})

index.tsで型をexportします

export type TodoApiRoutes = typeof todoApiRoutes
export type UserApiRoutes = typeof userApiRoutes

GitHubPackagesに登録(バックエンド側)

こちらの記事 を参考に
GitHubからtokenを作成し, .npmrcに

@{GitHubのユーザー名}:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken={ここにtokenを書きます}

を記載します。
.npmrcは.gitignoreに記載します。 dist以下に型を吐くように設定した上で

❯ bun run build
❯ npm publish

します。
(二回目以降はバージョンが重複しないようお気を付けください)

publish時にエラーがなければ、個人のプライベートなPackageとして登録されます。

GitHubPackagesから引いてくる(フロント側)

.npmrcに同様に

@{GitHubのユーザー名}:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken={ここにtokenを書きます}

を設定し、.gitignoreに記載します。
この時tokenは同一である必要はなく、readだけのtokenを発行する形の方が良いと思います。
.npmrcが存在する状態で

❯ bun install @{GitHubユーザー名}/hono-todo-backend

することでプライベートパッケージをインストールすることができます。

補完が効いていることを確認

レスポンスにはschemaファイルで決めた型が、クライアントの補完には実装したメソッドが保管されることが確認できました。

おわりに

honoのRPC機能を使いつつも、レポジトリを分割出来ることが確認できました。
今後はチームでの活用に向けて

  • GitHub Enterpriseのチーム内のPackagesへの登録

  • npm publishのGitHubActionsでの自動化 等をやっていきます。

参考

見よ、これがHonoのRPCだ
GitHub Packagesのnpm registryを利用してパッケージを公開してみた


執筆者プロフィール:Kenta Koshiishi
スタートアップ企業でFigmaを使用したUI設計から、Rustでのバックエンド開発、Reactでのフロントエンド開発まで、一気通貫で手掛けてきた。 SHIFTではシニアエンジニアとして入社し、既存のWebアプリに触れ始めた。

お問合せはお気軽に

SHIFTについて(コーポレートサイト)

SHIFTのサービスについて(サービスサイト)

SHIFTの導入事例

お役立ち資料はこちら

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/

PHOTO:UnsplashFotis Fotopoulos