はじめまして、株式会社タップルのKoigramでサーバーサイド開発を担当している安田航希(@_koki_yasuda)です。

はじめに

株式会社タップルでは現在「無料でも本気の恋ができる」がコンセプトの新しいマッチングアプリであるKoigramを運営しています。

Koigram

本記事では、そのKoigramを支えるサーバーサイドの全体像について、技術構成や選定したツール、その理由を詳しくご紹介します。

また、本記事はインフラストラクチャに関する技術選定に関しては言及しません。

目次

  1. はじめに
  2. 技術選定の基準
  3. ディレクトリ構成
  4. 管理画面
  5. CI・CD
  6. Lint
  7. テスト
  8. おわりに

技術選定の基準

Koigramのサーバーサイドの技術選定においては、タップルで利用している技術とのシナジーを重視しました。
技術スタックをできるだけ揃え、既存の知見や運用ノウハウを最大限に活用することで、開発効率の向上と安定したサービス提供を目指しています。
また、タップルでの経験から得た課題を踏まえ、同様の問題を避けるために、都度開発チームで話し合い最適な技術選定を行うように心がけました。

ディレクトリ構成

Koigramでは一部例外はありますが、単一のリポジトリでコードを管理するMonorepoを採用しています。
Monorepoを採用している背景として、サーバー・クライアントで共通のOpenAPI定義を扱いやすくすることに加え、コードの見通しを良くしたいという狙いがあります。

ディレクトリ構成としてはAPI(app)、管理画面(admin)、共通モジュール(common)といった粒度で切り分けています。
このように切り分けることによって各機能が独立し、変更の影響範囲を限定しやすくなっています。


├── server
│   ├── Dockerfile
│   ├── Makefile
│   ├── README.md
│   ├── admin
│   │   ├── package.json
│   │   ├── src
│   │   ├── tsconfig.json
│   │   ├── vitest.config.mts
│   │   └── vitest.workspace.mts
│   ├── app
│   │   ├── package.json
│   │   ├── src
│   │   ├── tsconfig.json
│   │   ├── vitest.config.mts
│   │   └── vitest.workspace.mts
│   ├── common
│   │   ├── package.json
│   │   ├── src
│   │   ├── tsconfig.json
│   │   ├── vitest.config.mts
│   │   └── vitest.workspace.mts
│   ├── docker-compose.yaml
│   ├── eslint.config.mjs
│   ├── package.json
│   ├── pnpm-lock.yaml
│   ├── pnpm-workspace.yaml
│   ├── tsconfig.base.json
│   └── turbo.json

commonディレクトリには、複数のアプリケーション(app、admin)で共通して利用されるコードやリソースを配置しています。
具体的には以下のようなものをcommonに含めるようにしています。

  • 共通の型定義
  • ユーティリティ関数(日付操作、画像操作など)
  • 共通のAPIクライアント
  • スキーマ定義

これらのコードは、特定のアプリケーションに依存せず、独立した機能を持つため、commonに配置することで再利用性を高めています。

また、appやadminとcommonの依存関係はTypeScriptのreferencesを利用して表現しています。
これにより、ビルド時間の短縮や循環依存の防止など、多くのメリットを享受できています。
commonの再利用性を損なわないため、appやadminからcommonへの依存は許可していますが、commonからappやadminへの依存は避けるようにしています。

現在サーバーサイドはTypeScript(Node.js)で開発を行っており、このようなディレクトリ構成になっていますが、ビルド時間の短縮を目的として将来的にはTurborepoを採用することを想定しています。
Turborepo導入後はapps(app, admin)、packages(common)のようにディレクトリが再構成される予定です。

さらに、パッケージ管理ツールにはpnpmを採用しています。
以前はnpmを使用していましたが、node_modulesが肥大化しやすく依存関係の解決に時間がかかるという課題がありました。
pnpmに移行したことで、パッケージのインストール速度が約2倍に向上し、開発効率が向上しました。

管理画面

管理画面ではAdminJSというライブラリを活用しています。

AdminJS

候補として様々なSaaS等があったのですが、以下のメリットを考慮してAdminJSの導入を決定しました。

  • NodeJS上で動くオープンソースのライブラリであり、APIと技術スタックを揃えられる
  • OSSなためセルフホスティングできる
  • Reactベースなため、カスタマイズ性も高い

これらのメリットに加え、SaaS型の管理画面ツールでは、外部サービスへの依存度が高くなり、セキュリティや可用性の面で懸念がありました。
AdminJSは、自社でホスティングすることでこれらのリスクを低減でき、SaaSと比較して利用料を抑えながら柔軟なカスタマイズができる点も採用理由の一つです。

AdminJSを導入した結果、管理画面におけるUIの開発工数が大幅に削減され、開発チームはアプリケーションのコア機能の開発に注力できるようになりました。
検索フィルターのカスタマイズや画面のリフレッシュの制御など細やかな制御等もカスタムコンポーネントを作成すれば実現可能なため、より複雑な制御をしたいケースでも、工数はかかりますが、現状要件を満たせないものはほぼなく、非常に満足しています。

CI・CD

CI・CDはGitHub Actionsに集約しており、開発効率向上のためワークフローはタップルで運用しているものを可能な限り再利用しています。
また、Koigramの各サーバーはECS(Fargate)上で稼働しており、ローカル実行やdryRunを手元で手軽に実行したくデプロイツールにecspressoを採用しています。

運用している中でecspressoのメリットを強く実感しています。具体的には下記の通りです。

  • verifyコマンドによってデプロイ前に環境変数が未設定の場合などの検知ができ、デプロイの失敗を事前に防ぐことができる
  • タスク定義を分けることでアプリケーションとインフラストラクチャで関心の分離ができる

特にecspressoのverifyコマンドはCIに組み込んで活用しており、デプロイ前に必ず実行することで、環境変数の設定ミスなどによるデプロイ失敗を未然に防いでいます。
例えば、タスク定義で参照している環境変数(FIREBASE_CONFI)がAWS Systems Managerのパラメータストアに設定されていない場合、以下のようなエラーを検知してデプロイを中断することができます。

❯ ecspresso verify --config ../../task-def/dev/app/0/deploy.yaml
2025/01/29 10:13:18 [INFO] ecspresso version: v2.4.5
...
2025/01/29 10:13:18 app-0/dev Starting verify
TaskDefinition
ExecutionRole[arn:aws:iam::xxx:role/ecs-tasks_dev0-app-execution]
--> [OK]
TaskRole[arn:aws:iam::xxx:role/ecs-tasks_dev0-app]
--> [OK]
ContainerDefinition[app-0]
Image[xxx.dkr.ecr.ap-northeast-1.amazonaws.com/sincere-app:latest]
--> [OK]
Secret FIREBASE_CONFIG[arn:aws:ssm:ap-northeast-1:xxx:parameter/dev-ecs/FIREBASE_CONFI]
--> [NG] ssm parameter /dev-ecs/FIREBASE_CONFI is not found
--> [NG] verify Secret FIREBASE_CONFIG[arn:aws:ssm:ap-northeast-1:xxx:parameter/dev-ecs/FIREBASE_CONFI] failed: ssm parameter /dev-ecs/FIREBASE_CONFI is not found
--> [NG] verify ContainerDefinition[app-0] failed: verify Secret FIREBASE_CONFIG[arn:aws:ssm:ap-northeast-1:xxx:parameter/dev-ecs/FIREBASE_CONFI] failed: ssm parameter /dev-ecs/FIREBASE_CONFI is not found
2025/01/29 10:13:20 [ERROR] FAILED. verify TaskDefinition failed: verify ContainerDefinition[app-0] failed: verify Secret FIREBASE_CONFIG[arn:aws:ssm:ap-northeast-1:xxx:parameter/dev-ecs/FIREBASE_CONFI] failed: ssm parameter /dev-ecs/FIREBASE_CONFI is not found

これにより、デプロイ時のエラーを未然に防ぎ、デプロイの失敗によるダウンタイムを最小限に抑えることができています。

現状タスク定義はjsonで管理しているのですが、環境での差分が少なく、定義が重複している場合が多いため、Jsonnetの移行を検討しています。
また、ECSサービスの管理がecspressoでできておらず、まだ十分にecspressoの恩恵を受けられていない部分があるため、順次対応していきたいと考えています。

Lint

静的解析ツールとしてはESLintを採用しています。
静的解析ツールはコードレビューの効率上げ、一定の品質担保、早期のバグ発見などメリットが多くあり、ESlintに関してはタップルでも導入実績があるため、Koigramでも採用することにしました。
タップルの設定を再利用することで、開発チームは既存のルールを活用しながら、効率的に開発を進めることができています。

具体的なルールとしては、タップルでの推奨ルールに加え以下のルールを採用しています。

  • complexity
  • import/no-restricted-paths
  • no-underscore-dangle

特にimport/no-restricted-pathsルールは、特定のディレクトリ間でのインポートを制限するために使用しており、
このルールを使用することで、プロジェクトのアーキテクチャを保護し、依存関係の循環や不適切な依存関係を防ぐことができています。

また、ESLint v10では、従来の .eslintrcファイルの設定形式である legacy config が廃止され、新しい設定形式である flat config が推奨されました。
そのため、Koigramでも設定を flat config に移行することにしました。

その際、Biomeへの移行も検討しましたが、現状は見送っています。
Biomeでは、no-floating-promisesのような型情報に依存するルールを適用する際に、API経由で型情報を取得する必要があり、現時点ではこの点が課題となっています。
ESLintであれば、TypeScriptの型情報を直接参照できるため、現時点ではESLintが最適であると判断しました。

ただ、BiomeをPoCで導入した際に実行速度がESLintに比べて大幅に高速で開発効率の向上に大きく貢献しそうだと考えているので、上記の課題が解決されるか、複雑度は増してしまいますが並列運用ができると考えているためまた検討したいと考えています。

テスト

単体テストフレームワークとしてVitestを採用しています。

リリース当初はJestを採用していたのですが、テストの実行速度を改善したいという課題がありました。
Vitestは、Jestと比較して高速なテスト実行が可能であり、ローカル環境でのテスト実行速度が約2倍に向上しました。
これにより、開発者はより迅速にテストを実行し、フィードバックを得られるようになり、開発効率が大幅に向上しました。

また、テストのカバレッジ計測には当初v8エンジンを採用していましたが、テストケースが増加するにつれてメモリリークが頻繁に発生するようになり、テスト実行が不安定になるという問題が発生しました。
この問題に対処するため、カバレッジプロバイダをistanbulへ移行しました。
istanbulは、Koigramの環境において安定したカバレッジ計測が可能で、メモリリークの問題も解消されたため、テスト環境の安定性が向上しました。

おわりに

ここまで読んでいただきありがとうございます。
Koigramのサーバーサイドの全体像についてご紹介しました。
まだまだ改善すべき点は多くありますが、ユーザーにより良いサービスを提供できるよう、今後も開発を進めていきます。