hitomaru — フリーランスエンジニアの業務をまるごと管理するWebアプリを作りました!
はじめに
はじめまして。個人事業主とメガベンでのエンジニアをしている者です。
普段の仕事の中で、稼働時間の記録、請求書の作成、経費の管理、確定申告の準備……と、コードを書く以外にやることが結構あるなと感じていました。既存の会計ソフトも試してみたのですが、準委任契約の上下割精算や源泉徴収の自動計算など、SES・フリーランスエンジニア特有の商慣習にぴったり合うものがなかなか見つかりませんでした。
それなら自分で作ってしまおう、ということで hitomaru というWebアプリケーションを開発しました。
この記事では、どんな機能を作ったのか、なぜその技術を選んだのか、開発の中で工夫したことなどをご紹介します。
どんなアプリなのか
hitomaru は、フリーランスエンジニアが日常的に行う業務を一つのアプリにまとめたものです。大きく分けると、以下のことができます。
稼働管理
案件ごとに日々の稼働時間を記録できます。SES契約でよくある「月140〜180時間の精算幅」にも対応していて、上下割・中間割といった精算方式を設定しておくと、月末に自動で請求金額を算出してくれます。
この精算計算は自分でも毎月手計算していたのですが、地味に面倒だったので真っ先に作りました。
請求書の発行
稼働実績をもとに、インボイス制度(適格請求書)に対応した請求書PDFを生成します。源泉徴収税の計算も組み込んでいるので、源泉徴収ありの案件でも金額を手計算する必要がありません。
源泉徴収は100万円以下なら10.21%、100万円を超える部分は20.42%というルールなのですが、これを毎回電卓で叩いていたのが嫌で自動化しました。
経費管理
日々の経費を科目ごとに記録できます。家事按分(事業とプライベートの使用割合)の設定もできるので、家賃や通信費など按分が必要な科目も正しく計算されます。
インボイス制度に関連して、仕入税額控除の対象かどうかも経費ごとに記録できるようにしています。
固定資産台帳
PCやディスプレイなど、10万円以上の資産を登録すると、定額法で減価償却費を自動計算します。少額減価償却資産の一括経費化にも対応しています。
年ごとの償却スケジュールを一覧で確認できるので、「この資産はあと何年で償却が終わるのか」がすぐにわかります。
確定申告の準備
ここが一番力を入れたところです。
1年間の売上・経費・減価償却を集計して、所得税の計算、青色申告特別控除の適用、各種所得控除の入力を経て、最終的な納付額(または還付額)を算出します。
さらに、その結果をもとに以下の書類を出力できます。
- 青色申告決算書PDF(損益計算書・減価償却明細・貸借対照表)
- 収支内訳書PDF(白色申告用)
- 確定申告書Bへの転記ガイド(どの欄にいくら書けばいいかの対応表)
消費税の簡易課税計算にも対応していて、課税事業者の方はみなし仕入率による消費税額の算出と、消費税申告書への転記ガイドも利用できます。
年度が終わったら「年度繰越」ボタンで翌年の元入金を自動計算し、次年度の設定に反映させることもできます。
技術スタック
| 領域 | 技術 |
|---|---|
| フレームワーク | Next.js 16 (App Router) |
| UI | React 19, Tailwind CSS 4 |
| 言語 | TypeScript 5 |
| データベース | PostgreSQL (Neon Serverless) |
| ORM | Drizzle ORM |
| バリデーション | Zod 4 |
| pdf-lib + Noto Sans CJK JP | |
| 認証 | 自前実装 (HMAC-SHA256) |
| ログ | Pino |
| エラー監視 | Sentry |
| テスト | Vitest, Playwright |
| ホスティング | Vercel |
以下、選定の意図をいくつかご説明します。
Next.js — APIもフロントも一箇所で
フロントエンドとAPIサーバーを分けるかどうかは最初に考えたポイントでした。結論として、Next.js の App Router を使って一つのプロジェクトにまとめています。
app/api/ にAPIを、app/(app)/ に認証後の画面を、app/(auth)/ にログイン画面を置く構成にしました。Vercelにデプロイするとそのまま動くので、インフラの管理コストがほぼゼロになります。個人開発ではこの手軽さがありがたいです。
セキュリティ面では、Next.js 16 で使える proxy.ts に CSRF 検証とレートリミットを入れています。全リクエストの入口で一括処理できるので見通しが良く、気に入っています。
Drizzle ORM — PostgreSQL の機能を素直に使いたかった
ORMは Drizzle を選びました。Prisma と比較検討したのですが、PostgreSQL の機能(ENUM型、CHECK制約、複合外部キーなど)をそのまま使いたかったのが大きな理由です。
hitomaru ではマルチテナントのデータ分離に複合外部キーを使っていまして、例えば請求書テーブルからクライアントテーブルへの参照を (clientId, userId) のペアで張っています。こうすることで、あるユーザーの請求書が別のユーザーのクライアントを参照することがDB制約のレベルで不可能になります。
Drizzle はスキーマ定義が TypeScript そのものなので、型推論がよく効きます。SQLを書いている感覚に近いのに、型の恩恵はしっかり受けられる。このバランスが良かったです。
Neon PostgreSQL — サーバーレスとの相性
データベースは Neon を使っています。Vercel Functions からの接続ではHTTPベースのコネクションプーリングが使えるので、サーバーレス特有のコネクション枯渇問題を気にしなくて済みます。
ローカル開発では通常の pg ドライバに自動で切り替わるようにしているので、開発体験も損なわれません。個人開発の段階ではほぼ無料で運用できる点もありがたいです。
認証 — NextAuth を使わなかった理由
認証は自前で実装しました。NextAuth を使わなかったのは、いくつかやりたいことがあったためです。
一つは、パスワード変更時に全デバイスのセッションを即座に無効化する機能です。ユーザーテーブルに sessionVersion というカラムを持たせていて、これをインクリメントするだけで、発行済みのセッショントークンがすべて無効になります。
もう一つは、認証周りのエラーメッセージを日本語で統一したかったことです。ライブラリ経由だとメッセージのカスタマイズに限界があるので、自前のほうが都合が良いと判断しました。
セッショントークン自体は HMAC-SHA256 で署名した JWT 風のトークンを httpOnly Cookie に格納するシンプルな方式です。
PDF生成 — サーバーレスで日本語PDFを出す工夫
請求書や決算書のPDFを生成する必要があったのですが、Puppeteer のようなヘッドレスブラウザ方式はサーバーレス環境ではサイズ的に厳しいため、pdf-lib を採用しました。ピュアJavaScriptのライブラリなので、Vercel Functions でも問題なく動きます。
日本語を描画するために Noto Sans CJK JP フォントをバンドルしています。ちょっと工夫が必要だったのは、日本語と英数字が混在するテキストの扱いです。金額表示(例: ¥1,234,567)を全角フォントで描画すると数字の字間が広くなりすぎるので、1文字ずつ日本語か英数字かを判定して、フォントを切り替えて描画するようにしました。
現在、以下の3種類のPDFを出力できます。
- インボイス対応の請求書
- 青色申告決算書(損益計算書・減価償却明細・貸借対照表)
- 収支内訳書(白色申告用)
ドメインロジックの設計 — 計算は全部純粋関数に
税金や請求の計算ロジックは lib/domain/ というディレクトリにまとめて、すべて純粋関数として実装しました。データベースにもAPIにも依存せず、入力を渡すと計算結果が返ってくるだけの関数です。
こうした理由は主にテストのしやすさです。税金の計算が正しいかを検証するのにDBのモックを書くのは大げさですし、バグがあったときに原因の切り分けが面倒になります。純粋関数にしておけば、テストは入力と出力の突き合わせだけで済みます。
実装したドメインロジックは以下の通りです。
| ファイル | 内容 |
|---|---|
| billing.ts | 稼働精算(固定/上下割/中間割)、源泉徴収税 |
| income-tax.ts | 累進課税、青色申告特別控除、復興特別所得税、各種控除 |
| consumption-tax.ts | 消費税(簡易課税)、みなし仕入率、地方消費税 |
| depreciation.ts | 定額法、月割、少額資産一括経費化、備忘価額 |
| invoice-compliance.ts | 適格請求書の要件チェック |
| payment-reconciliation.ts | 入金消込(未入金/一部入金/入金済/延滞) |
| year-end-carryover.ts | 年度繰越、翌年元入金の計算 |
消費税の計算では、JavaScriptの浮動小数点演算で1円ずれる問題にハマりました。1100000 / 1.1 が 1000000 にならず 999999.999... になるケースがあるのですが、1100000 * 100 / 110 と書き換えることで整数同士の演算に帰着させて解決しています。税金の計算では1円の誤差も許容できないので、こうした細かい部分は気を遣いました。
テストは Vitest で書いていて、現在 467 ケースあります。テスト名は日本語にしています。日本の税制を扱っているので、「第5種(サービス業)で正しく計算される」のように書いたほうが、何を検証しているかがわかりやすいと思いました。
フロントエンドの方針 — 最小限の抽象化
状態管理ライブラリは導入していません。React の useState と3つのカスタムフック(useFetch / useForm / useAsync)で全画面をまかなっています。
- useFetch: APIからのデータ取得。loading / error 状態の管理
- useForm: フォームの値管理と Zod スキーマによるバリデーション
- useAsync: 保存・削除などの非同期操作。loading 中のボタン制御
Redux や Zustand を入れるほどの複雑な状態管理が今のところ必要なかったので、シンプルに保っています。必要になったら入れればいい、という考え方です。
バリデーションは全APIエンドポイントで Zod を使っています。フロントエンドのフォームバリデーションにも同じスキーマを使えるので、二重定義にならずに済んでいます。
セキュリティ面で気をつけたこと
個人の確定申告データを扱うアプリなので、セキュリティには気を配りました。主な対策をまとめます。
- CSRF対策: Originヘッダーの検証を全APIリクエストに適用
- レートリミット: IPベースで全体100リクエスト/分、認証系はさらに個別制限
- 暗号化: マイナンバーや銀行口座情報は AES-256-GCM で暗号化して保存
- ログのPIIマスキング: メールアドレスやパスワードがログに出力されないよう自動マスク
- セキュリティヘッダー: HSTS、CSP、X-Frame-Options などを設定
- テナント分離: 全クエリに userId フィルタを付与 + 複合外部キーによるDB制約
確定申告ダッシュボード — すべてがつながるところ
個人的に一番気に入っている機能が、確定申告ダッシュボードです。
画面上にステップバーがあり、「経費確認 → 按分設定 → 減価償却 → 控除入力 → 計算結果」という流れで作業を進められます。各ステップのページで入力や確認を行い、最後のダッシュボードに戻ると、1年分のデータが集計されて以下のような情報が表示されます。
- 事業所得、所得税+復興税、源泉徴収税額、最終的な納付額/還付額
- 科目別の経費内訳(按分前・按分率・按分後)
- 確定申告書のどの欄にいくら書けばいいかの転記ガイド
- 青色申告決算書や収支内訳書のPDFダウンロード
日々コツコツ記録したデータが年末に一つのダッシュボードに集約される瞬間は、作っていて一番うれしかったところです。
規模感
参考までに、現時点での数字を載せておきます。
| 項目 | 数値 |
|---|---|
| TypeScript ファイル数 | 200以上 |
| テストケース数 | 467 |
| DBテーブル数 | 17 |
| APIエンドポイント数 | 40以上 |
| 画面数 | 25以上 |
| 本番依存パッケージ | 43 |
おわりに
hitomaru は、自分自身がフリーランスとして感じていた不便さを解消するために作ったアプリケーションです。
技術的には特別なことはしていなくて、Next.js、Drizzle、Zod、Tailwind CSS といった今どきのスタックを組み合わせているだけです。ただ、それを日本の税制やフリーランスの商慣習というドメインに丁寧に当てはめていく作業は、想像以上に奥が深く、楽しいものでした。
消費税の端数処理で1円ずれて悩んだり、減価償却の月割計算の仕様を国税庁のサイトで何度も確認したり、そういう地味な作業の積み重ねが、最終的に「ボタン一つで決算書が出る」という体験につながっています。
