‹ Mockun Note

Hono + PGLite + Reactでミニマルなテンプレートを作った

Posted Nov 14, 2024

以前、会社で使う業務システムの開発にBun + Hono + Drizzle + SQLiteを使い始めたことを書きました。

基本機能が出来上がって、改善点が出てきたので、今後の開発のためのテンプレートとして整備していくことにしました。

今回構築したテンプレートでは、前回構築した社内システムの環境から引き続きBun + Hono + Drizzleを使いつつ、DB部分をPostgreSQLのWASM実装(?)であるPGLiteに切り替えることにしました。

ソフトウェアエンジニアではない自分がどのようなことを考えてテンプレートを作ったのか、備忘として残しておきます。

前回の環境

経営している会社で使う業務システムのためにBun + Hono + SQLiteを使い始めたことは前回書きました。

前回の構成では、VPS(fly.io?)にデプロイする手前までは、この動画に沿って作業を進めていき、ある程度開発の流れがわかってきたところで、自分なりに変更しています。

チュートリアルからの変更点

  • SQLiteを使う

バックエンドとして、Hono、Zod、Drizzleは上記チュートリアル通り利用しています。

データベースについては、動画ではPostgreSQLを使っていますが、DBサーバーを立てたり、管理するのが面倒なので、SQLiteを利用するように変更しました。

SQLiteであれば、わざわざDBサーバーを構築・管理することなく、ファイル単位で管理することができるため、運用が楽になります。

中・大規模のシステムならDBサーバーを立てるメリットもあるかもしれませんが、現時点ではSQLiteのような簡易的なデータベースで十分です。

フロントエンドは動画の通り、ReactとTailwindを使います。個人的にフロントエンドの開発にあまり触れたことがなく、門外漢であるため何も思うところはありませんでした。

気になるところ

自社で使う業務システムを開発していくうちに、いくつか気になるところ、改善したいところが出てくるようになりました。

  • ディレクトリ構成がわかりにくい
  • SQLiteでmigrateができない(?)
  • コンテナ化されていない

ディレクトリ構成がわかりにくい

動画では、バックエンドを先に作り、あとからフロントエンドを追加する手順になっています。ディレクトリ構成やライブラリ管理においてバックエンドとフロントエンドで分けにくくなっているように感じました。

どちらかというと、自分自身の経験不足、理解が浅いことが原因ではあるのですが、数ヶ月後にはどこに何があるか忘れてしまうだろうな、という感覚です。

SQLiteでmigrateができない(?)

忘れてしまったのですが、SQLiteを使っていて既存のテーブルを変更する際に、対応していないケースがありました。(何かALTER TABLE XXXを実行するケースだった気がする。)

結果的に毎回マイグレーション用のSQLファイルを削除して運用していました。データのバックアップ、テーブルの変更、データのリストアと面倒くさい作業を手作業でやっていました。これではマイグレーション機能の意味がありません。

データベースがPostgreSQLならALTER TABLEにも対応しているのですが、すでに述べた通り、DBサーバーを立てる手間はかけたくありません。

コンテナ化されていない

チュートリアルを途中まで見たかぎりでは、コンテナ化はしていないようだったので、前回の業務システムでは個別にコンテナ化していました。

テンプレートとしてコンテナ化までカバーしておけば、インフラを意識せず様々な環境で動かせるので、押さえておきたいところです。

テンプレート化する際の改善

  • ディレクトリ構成の見直し
  • SQLiteからPGLiteへの変更
  • コンテナ化を組み込む

ディレクトリ構成の見直し

hono-templateという一つのリポジトリの中に、バックエンドとフロントエンドをそれぞれ入れます。

Dockerfile、ビルドや起動用のこまごましたshellスクリプトはcontainerディレクトリに入れます。

hono-template
├── backend          # Hono,Drizzle,PGLiteのバックエンド
│   ├── drizzle      # Drizzleのマイグレーションファイル
│   ├── node_modules
│   ├── pgdata       # PGLiteのデータファイル
│   └── src
├── container        # Dockerfileとアプリ起動スクリプト
└── frontend         # React,Tailwind CSSによるフロントエンド
    ├── node_modules
    ├── public
    └── src

開発しながらローカルで動かす場合には、backendディレクトリとbackendディレクトリの中でそれぞれbun run devを実行し、フロントエンドのサーバーであるhttp://localhost:5173にアクセスすればOKです。

これはチュートリアルの動画を踏襲しています。

SQLiteからPGLiteへの変更

SQLiteを利用した場合に一部のマイグレーション機能がうまくいかないので、PGLiteに乗り換えます。

DBサーバーを立てず、ファイルシステムだけで使える手軽さと、Drizzleのマイグレーション機能がフルで使えるであろうPostgreSQL、というのは非常に魅力的です。

インストールはDrizzleのドキュメントの通り実行するだけです。

bun add @electric-sql/pglite

ハマったのが、PGLiteでマイグレーションを実行するための設定です。

backend/drizzle.config.tsに設定を書くのですが、dialectsqliteからpostgresqlに変更するだけでは「driverがない」といわれてしまいダメでした。

エラーメッセージに言われた通り、driverとしてpgpostgresをインストールすると「url」の書き方でエラーが出てしまいます。

ネットでいろいろ調べて、driver"pglite"を指定するとあっけなく動いてしまいました。PostgreSQL用のpgpostgresなどのドライバーは不要みたいです。

import { defineConfig } from "drizzle-kit"

export default defineConfig({
    dialect: "postgresql",
    schema: "./src/db/schema",
    out: "./drizzle",
    driver: "pglite",           # driverとして"pglite"を指定
    dbCredentials: {
        url: "./pgdata/",
    },
});

カラムの追加と削除でマイグレーションの確認しましたが、動いたのでSQLiteでできなかった機能も動いていそうです。

コンテナ化を組み込む

1つのコンテナにフロントエンドとバックエンドを詰め込んだ構成です。

チュートリアルの動画の通り、フロントエンドはHonoの静的ファイルとしてサーブします。

将来的にフロントエンドの規模が大きくなれば、外部のCDNに置くなど考える必要も出てくるかもしれませんが、とりあえずこのままにします。

// (省略)
const app = new Hono()
app.use('*', logger())

const apiRoutes = app.basePath('api')
    .route('/hello', helloRoute)

app.get('*', serveStatic({ root: '../frontend/dist', precompressed: true }))
app.get('*', serveStatic({ path: '../frontend/dist/index.html', precompressed: true }))
// (省略)

以下のDockerfileをMac M1 Proでビルドしたコンテナイメージはだいたい195MB前後でした。

oven/bun:1.1.34-slimが170MBくらいなので、プロジェクト固有のサイズで25MBくらい追加されていますね。

なんの機能も実装していない状態のコンテナでこのサイズってこんなものなんでしょうか。よくわかりません。

# Install and Update packages
FROM oven/bun:1.1.34-slim AS base
RUN apt-get update 
RUN apt-get install -y \
    python3 \
    python3-pip \
    build-essential \
    sqlite3 \
    libsqlite3-dev
RUN apt-get upgrade -y

# Install application
FROM base AS install

# Install Frontend Dev
RUN mkdir -p /temp/frontend/dev
COPY frontend/package.json frontend/bun.lockb /temp/frontend/dev/
RUN cd /temp/frontend/dev && bun install --frozen-lockfile

# Install Frontend Production
RUN mkdir -p /temp/frontend/prod
COPY frontend/package.json frontend/bun.lockb /temp/frontend/prod/
RUN cd /temp/frontend/prod && bun install --frozen-lockfile --production

# Install Backend Production (NOT need to build, so only for production)
RUN mkdir -p /temp/backend
COPY backend/package.json backend/bun.lockb /temp/backend/
RUN cd /temp/backend && bun install --frozen-lockfile --production

# Copy application modules
FROM base AS frontend_build
WORKDIR /usr/src/app/frontend
COPY --from=install /temp/frontend/dev/node_modules node_modules/
COPY --from=install /temp/frontend/prod/node_modules node_modules/
COPY frontend/. .

# build frontend
ENV NODE_ENV=production
RUN bunx --bun vite build

# Copy production dependencies and source code into final image
FROM oven/bun:1.1.34-slim AS release
WORKDIR /home/bun/app
COPY --from=frontend_build /usr/src/app/frontend/dist frontend/dist/
COPY --from=install /temp/backend/node_modules backend/node_modules/
COPY backend/tsconfig.json backend/
COPY backend/src backend/src/

# Run the application
ENV NODE_ENV=production
USER bun
COPY docker/start.sh /home/bun/app/
WORKDIR /home/bun/app
ENTRYPOINT [ "/bin/bash", "start.sh" ]

テンプレートを作ろうと思った理由

そもそもなぜこのテンプレートを作ろうと思ったのか。簡単にいえば、「早い、安い、うまい」を実現したいと思ったからです。

早い

テンプレート、つまり「基本の型」を持っておくことで、作り始めるまでのハードルを下げていきたいと考えています。

ある程度事前に選んでおいたミドルウェアや構成をテンプレートとして持っておき、何かを作る際にはそのテンプレートをもとにクイックに作っていくようにしたかったのです。

開発言語も、ランタイムも、フレームワークも、DBMSも、ディレクトリ構成も、インフラも、それ自体は課題解決には直結しません。機能を実装することが付加価値を生みます。

テンプレートを使って、暗黙的な制約を設けることで、アプリケーションの実装に集中し、早期に課題解決に繋げる、そんな進め方を実現したいと考えています。

安い

言語にもフレームワークにもこだわりはありませんが、インフラ費用を最小化するためにも軽量に動作するソフトウェアスタックを選定する必要があると考えています。

軽量に動作すれば、より低いスペックのマシン(VM含む)で動作させることができます。今回選定したBun+Honoの組み合わせは、同スペックのマシン上で動作する従来のRuntimeやフレームワークと比べてハイパフォーマンスだと言われています。

アプリに組み込むような形で使えるSQLite、PGLiteを使うことで、DBサーバーを立てない構成にしているのもミニマルなインフラ構成で動かすための工夫の一つです。

これをコンテナ化してCloud Runで動かせば、利用がない時間帯には自動でスケールインさせることで、インフラ費用を最小化できます。

うまい

「早い、安い、うまい」は、最後にいい感じにまとめたくて選びましたが、正直「うまい」はあまり考えてなかった。

あえて挙げるなら、(1)得意な構成を持っておく、(2)他人に依頼する場合でもコードを追えるようにする、(3)どうせなら今時のランタイムとフレームワークを使う、の3点でしょうか。

(1)得意な構成を持っておく

自分の「基本の型」を持っておきたいと考えてテンプレートを作った、という面もあります。

プログラミングは得意ではありませんが、何か動くものを作れるというのは単純に楽しいです。

ホビープログラマとしてものづくりをするなら得意な構成の一つくらい持っておきたいものです。

得意な構成を一つ押さえておけば、応用は比較的しやすいと考えています。

(2)他人に依頼する場合でもコードを追えるようにする

私自身は、ソフトウェアエンジニアではありません。実際のプロジェクトにおいては、プロのソフトウェアエンジニアにチームに参加してもらう必要があります。

どこに何を書くべきか、ある程度の制約に沿って作ってもらうことで、ある程度コードを追える状態でプロジェクトを進めることができると考えています。

完全な実装はできなくとも、コードを追えるようにしておくのは、エンジニアがチームを抜けた後も、クライアントに対する責任を負う必要があるためです。

(3)どうせなら今時のランタイムとフレームワークを使う

完全に個人の趣味の問題ですが、どうせなら今時のものを使いたいという気持ちがあります。

Bunも、Honoも、PGLiteも、個人的な感覚では今時のものだと思っているので、どうせテンプレートを作るならこれらを組み合わせて使いたいと思いました。

どのみちビジネスの課題を解決するのであれば、触っていて楽しいものを、と思います。

ちなみにHonoは開発者が日本人らしいですね。名前も日本語の「炎」から来ているようで、なんだか親しみを感じます。ぜひ頑張って欲しいです。

今回採用したOSSを使ったプロダクトや受託開発の仕事が成功した際にはOSS活動にも還元していきたいと思います。

さいごに一言

Bun + Hono + Drizzle + PGLiteのバックエンドと、React + Tailwind CSSのフロントエンドでWebアプリのプロジェクトテンプレートを作りました。

個人的には、何かアイデアが浮かんで、Webアプリを作ろうとした時に、すぐに機能要件を考え始められるようになるのは、かなり嬉しいことなんです。

このテンプレートを個人開発や仕事で積極的に活用して、大事に育てていきたいと思います。

(お察しの方もいると思いますが、フロントエンドはあまり興味がない。というか得意ではないんです。なんとなーくわかるReactと、ネットに情報が多いTailwindを選択しています。)

🏷