Next.js×NestJSをモノレポで構築/運用してみました

f:id:golazooo23:20220407164946p:plain

こんにちは、スタメンエンジニアの手嶋です。普段はRuby on RailsやReactなどの技術を用いて開発しています。最近はフィーチャーチーム体制に切り替わったこともあり、AWSなどの技術にも触れる機会が増えました。

これまで複数のプロジェクトにおいてReact(TypeScript)で開発を行ってきました。そんな中でやはり型の恩恵を感じることが多かったのですが、バックエンドも含めてTypeScriptでアプリケーションを作成してみたいという想いが湧いてきたので、個人開発としてNext.jsNestJSで構築したアプリケーションをモノレポで運用し本番環境で動かしてみました。

モノレポはその名の通り、単一のリポジトリ(git等)で複数のプロジェクトを管理することです。 主に以下のようなメリットを享受できないかと思い、モノレポを採用しました。

  • ESLint / Prettierなどの設定を一元管理できる
  • frontend/backendを同時に変更する時に管理しやすくなる

本記事では Next.js/NestJSとモノレポ で実際に運用するまでに試した方法やリリースまでに苦戦した点を中心に紹介したいと思います。

※上記に焦点を当てているため、Next.jsNestJS等個々の技術に関する詳細説明は割愛しております。

目次

  • 完成イメージ/技術スタック
  • フロントエンドバックエンド共通
  • フロントエンド
  • バックエンド
  • まとめ

完成イメージ

早速ですが、モノレポでの大まかな完成形イメージは以下のようなディレクトリ構成となります。ルート配下にfrontendbackendというディレクトリを持ち、それぞれの中でNext.jsNestJSを作成しています。

// アプリケーション全体の構成
root─┬─ frontend #Next.js
     │  ├─ package.json #frontend固有のpackageを管理 
     │  └─ その他のファイル 
     │
     ├─ backend #NestJS
     │  ├─ package.json #backend固有のpackageを管理 
     │  └─ その他のファイル 
     │ 
     ├─ package.json #共有で扱うpackageを管理
     └─ yarn.lock #全体で1つ

各ディレクトリ配下で個別にpackageを管理するために、それぞれpackage.jsonを作成していますが、ルートでもpackage.jsonを管理しています(計3つ)。ルートのpackage.jsonで管理するのはeslintprettierなど、アプリケーションで共通となるパッケージです。

※yarn.lockに関しては、全てのpackageの依存を管理した上で、アプリケーション内に1つだけ生成されるようです。

技術スタック

詳細に触れないものもありますが、以下のような技術を使用しています。

  • Backend
    • NestJS
    • Prisma
    • GraphQL / Apollo
  • Frontend
    • Next.js / React
    • GraphQL Code Generator / Apollo Client
  • その他
    • yarn workspace
    • ESLint / Prettier / husky
    • Docker
    • Vercel(Next.jsをホスティング)
    • heroku(NestJSをホスティング)

フロントエンドバックエンド共通

最初にフロントエンドバックエンドで共通となる部分の紹介です。

上述のように同じアプリケーション内でフロントエンドとバックエンドをそれぞれを管理するために、WorkspacesというYarnの機能を使用しています。(追加のpackageなどは特に必要ありません。) こちらを使用することで、ひとつのリポジトリで複数のパッケージを開発できるようになり、効率的に作業を進められるようになります。

例えば、ルートのpackage.jsonを以下のような記述にする事で、その配下のfrontendbackendという複数のpackage.jsonの管理をすることができます。 またルートから各ディレクトリを参照できるようになるので、backendに追加のパッケージをインストールしたい場合はyarn backend add パッケージ名というコマンドをルートで実行するだけでよく、各ディレクトリへ移動する必要がありません。 ※注意点として、ここでpackageというキーに指定する値はディレクトリの名前ではなく、各ディレクトリ内のpackage.jsonのname属性の値と統一させる必要があります。

nohoistに関しては必須ではないと思いますが、実際の運用にあたって必要だったため導入しています。詳細はバックエンドの章で後述します。

{
  "name": "app-name",
  "private": true,
  "workspaces": {
    "packages": [
      "frontend",
      "backend"
    ],
    "nohoist": [
      "**/backend",
      "**/backend/**",
      "**/frontend",
      "**/frontend/**"
    ]
  },
  "scripts": {
    "backend": "yarn workspace backend",
    "frontend": "yarn workspace frontend"
  },
  // その他devDependenciesなどを記述する
}

バックエンド

バックエンドはNestJSで構築し、ホスティング先には無料で使えるherokuを選びました。クライアントとGraphQLで通信するために、NestJs側にもGraphQL / Apolloをインストールしました。

モノレポで管理するにあたってポイントとなったのは以下2点です。

1つ目はdefalutブランチにマージした際にbackend配下のみの変更を検知してherokuへのデプロイを開始することです。(frontendの変更のみであればデプロイをしない)

こちらに関してはgithub/actionsを使って実現しました。 プロジェクト内に以下のようなファイルを作成した上で、今回はherokuへのデプロイなのでHEROKU_API_KEYのAPIをgithubのsettingに設定しました。

name: Deploy backend

on:
  push:
    branches:
      - main
    paths:
      - "backend/**"

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Release app backend
        uses: akhileshns/heroku-deploy@v3.0.4
        with:
          heroku_api_key: ${{secrets.HEROKU_API_KEY}}
          heroku_app_name: "app_name"
          heroku_email: "hoge@gmail.com"
        env:
          HD_APP_BASE: "backend"

2つ目はルートのpackage.jsonにおけるnohoistの設定です。 ORMとしてPrismaを導入したのですが.envに記載するDATABASE_URLを参照できずPrismaを起動できないという問題に直面しました。調査の結果、プロジェクト(ルート)レベルのnode_modulesPrismaがインストールされてしまうと正常に動作しないということが分かりました。

解決策として、Workspacesnohoistの設定で各ディレクトリを指定することでプロジェクトレベルではなくパッケージレベル(ここではfrontendbackendを指します)のnode_modulesにインストールすることでこの問題を解決しています。 ※ディレクトリの名前ではなく、ディレクトリ内のpackage.jsonのnameの値と統一する必要があります。

{
 // その他の記述
  "workspaces": {
    "packages": [
      "frontend",
      "backend"
    ],
    "nohoist": [
      "**/backend",
      "**/backend/**",
      "**/frontend",
      "**/frontend/**"
    ]
  },
  // その他の記述
}

このようにWorkspacesはその性質上、デフォルトではすべてのpackageがルートのnode_modulesに入ってしまいますが、各ディレクトリレベルで管理したいpackageはそのディレクトリで管理した方が問題が起きにくいと考えています。

フロントエンド

続いてフロントエンドです。フロントエンドはNext.js/Reactで構築し、最もメジャーなVercelにホスティングしました。バックエンドにGraphQLでリクエストするためにApollo Clientを導入しています。またバックエンドを含めた型の統一管理や、型を記述するコストを削減するためにGraphQL Code Generatorを試してみました。

実際の運用にあたってのポイントになったのは以下2点です。

1つ目はVercelRoot Directory設定の変更です。 上述のように各ディレクトリで、それぞれに必要なパッケージをインストールしています(Next.jsfrontend)が、Vercelのデプロイ等の各設定はルートにNext.jsアプリケーションが構築されることを前提としている為、その部分の手当てをする必要がありました。

2つ目は余分なデプロイ処理を行わないようしたことです。 前章(バックエンド)の内容と重複する部分がありますが、基本的にデプロイは以下のように、各ディレクトリに対して差分があるPRがマージされた時のみ行いたいという考えました。

  • frontendの差分のみ -> Vercelへデプロイ
  • backendの差分のみ -> herokuへデプロイ
  • 両方差分がある場合は2つのデプロイ処理を実行

この2つを実現する為にVercel上で以下二箇所を変更しました。

1.Root Directoryの変更 Settingsタブ/Generalで表示されているRoot DirectoryNext.jsをインストールしているディレクトリに変更します。 今回はfrontendという命名にしている為、そちらに合わせる形にしています。

f:id:golazooo23:20220407005533p:plain

2.Ignored Build Stepの設定 VercelではIgnored Build Stepを設定することで、特定の条件においてcommit及びマージ時のBuild処理をスキップすることが可能です。こちらはSettingsタブ/Gitで設定可能で、ファイルの実行を指定することなどもできますが、今回は単純にディレクトリ指定としています。

f:id:golazooo23:20220407005554p:plain

まとめ

以上が、Next.js×NestJSをモノレポで実際に運用する方法と、リリースまでに苦戦した点です。実際には他にも色々な躓きポイントがありましたが、今回はTypeScript×モノレポという点に絞って紹介させていただきました。

Next.jsNestJS共にホスティング先は様々な選択肢があるため、完全に同じような構成にはならないと思いますが、TypeScript×モノレポでアプリケーションの構築及び運用を目指す方にとって少しでも参考になれば幸いです。

今回は個人開発で扱う技術スタックに関する紹介となりましたが、弊社の事業も拡大や多角化に伴い、開発組織が直面する課題や求められる能力/技術スタックが日々変化しています。 加えて、弊サービスのTUNAGでは、お客様である企業の成長にエンゲージメントという側面から貢献するために、まだまだ開発すべきことがたくさんあります。

エンジニアとして、やりがいのある仕事がしたい!わくわくする仕事をしたい!という方がいらっしゃいましたらぜひ、弊社採用窓口までご連絡ください。

TUNAGの開発体制、使用技術については下記ブログ記事を参照ください。 TUNAGの技術と開発体制のすべてを紹介します!

また株式会社スタメンでは、オンラインサロン用プラットホームFANTSというサービスも開発・運営しております。 ご興味がある方はぜひ下記のリンクをご覧ください。 FANTSの開発技術・開発組織を紹介します!

参考にさせていただいた資料