Amazon Web Services ブログ

Nx と AWS Amplify Hosting を用いて Next.js アプリ間でコードを共有する

この記事は、Share code between Next.js apps with Nx on AWS Amplify Hosting を翻訳したものです。

この記事では、AWS Amplify Hosting がモノレポ、特に Nx と連携し、モノレポで管理されているフロントエンドアプリケーションをデプロイする機能を探ります。銀行の仕様に合わせてブランディングできるライブラリやコンポーネントで構成された、同じ住宅ローン計算機を使用する複数の銀行のウェブサイトの例を通して、モノレポを使用する利点を学びます。このシナリオの例では、私たちがいくつかの銀行ブランドを所有する大規模な金融機関で働いているとしましょう。この金融機関を BankCorp と呼び、このブランドの下にある銀行の例を BankA と BankB とします。

Nx はスタンドアロンのプロジェクトや、複数のバックエンド、フロントエンド、共有ライブラリのプロジェクトのモノレポを管理するためのオープンソースのビルドシステムです。

モノレポ(複数のフロントエンドアプリケーションと共有ライブラリを含む単一のリポジトリ)がある場合、その複雑さを管理するためのツールが必要です。従来、開発者は npm、Yarn、pnpm のようなパッケージマネージャを使って依存関係をインストールし、モノレポをビルドしてきました。しかし、これらのツールはモノレポ専用に設計されていないため、規模が大きくなると使い勝手が悪くなります。

Nx はこのような複雑性に対処し、管理するすべてのパッケージとアプリケーションに一貫性を提供するために作られました。

注: このプロジェクトの完全なソースコードを含むリポジトリはこちらにあります。

モノレポのアプリ、ライブラリ、コンポーネントをビルドする

まず、アプリケーションや共有ライブラリ、コンポーネントを置くモノレポを生成する必要があります。Nx はモノレポを格納する「ワークスペース」を作成するコマンドラインユーティリティを提供しています。

以下のコマンドを実行して、Integrated Monorepo | Nx に従って新しいワークスペースを作成します。

npx create-nx-workspace@latest banking-web-apps --preset=ts

“Enable distributed caching to make your CI faster” というプロンプトには、“No” を選択します。

これでワークスペースができたので、共有ライブラリとコンポーネントを組み合わせて、銀行ウェブアプリケーションをビルドすることができます。

モノレポ管理に Nx を使用する理由

モノレポは、ライブラリやコンポーネントのデプロイプロセスを簡素化するのに役立ちます。モノレポがなければ、各パッケージを個別にデプロイし、パッケージのバージョンや依存関係を追跡し、バージョンの互換性を確保しなければなりません。これは、パッケージの数が増えるにつれて、指数関数的に複雑になる可能性があります。モノレポでは、すべてのパッケージや依存関係が単一のリポジトリに含まれます。パッケージ間での変更をアトミックに行い、それらを一緒にデプロイすることができます。パッケージのバージョンは自動的に同期され、パッケージ間の変更を検証するためには一組のテストを実行するだけで済みます。新しいバージョンのデプロイは、メインブランチの最新コミットをデプロイするだけで簡単です。モノレポはライブラリとコンポーネントを個別にバージョニング、テスト、デプロイするときの複雑さを大幅に取り除いてくれます。

私たちの要件では、2 つの銀行アプリ間で住宅ローン計算機を共有します。この分離は機能を集約し、重複を減らすためのもので、このアーキテクチャは基礎となるアプリケーションの様々な部分を異なるチームや開発者が制作している大企業やチームにとって有益です。

モノレポは、アプリ間でライブラリやコンポーネントを共有するような機能を収容するのに最適です。Nx はワークスペース、ライブラリ、パッケージ、アプリケーションを生成し、それらをカスタマイズ可能なインポートでパッケージレイヤーを介して接続するいくつかの便利な機能を提供します。ジェネレータは豊富で、Next.js、React、Angular、Nuxtのフロントエンドフレームワークと Express のバックエンドフレームワークに対応しています。

Nx で住宅ローン計算機ライブラリを生成する

住宅価格、頭金、金利、借入期間から住宅ローンの月々の支払額を計算する関数が必要です。

次のコマンドを実行して、住宅ローン計算機のコードを配置するためのワークスペースを生成します。

訳者注: ドキュメントを参考に Nx をインストールするか、npx コマンドを利用してください。

$ nx generate @nx/js:library mortgage-lib

テストランナーを選ぶプロンプトには vitest を選択します。またワークスペースが TypeScript 用に設定されているため、バンドラーとして tsc が選択されます。

>  NX  Generating @nx/js:library

✔ Which unit test runner would you like to use? · vitest
✔ Which bundler would you like to use to build the library? · tsc

次のディレクトリとファイルが packages ディレクトリの下に生成されます。

packages
└── mortgage-lib
    ├── README.md
    ├── package.json
    ├── project.json
    ├── src
    │   ├── index.ts
    │   └── lib
    │       ├── mortgage-lib.spec.ts
    │       └── mortgage-lib.ts
    ├── tsconfig.json
    ├── tsconfig.lib.json
    ├── tsconfig.spec.json
    └── vite.config.ts

caclulateMortgage 関数を mortgage-lib.ts の中に配置することができます。

// packages/mortgage-lib/src/lib/mortgage-lib.ts
type LoanOptions = {
  homePrice: number;
  downPayment: number;
  interestRate: number;
  loanTerm: number;
  currency: string;
};

export function calculateMortgage({
  homePrice,
  downPayment,
  interestRate,
  loanTerm,
  currency,
}: LoanOptions): string {
  // Mortgage formula: M = P * (r(1 + r)^n) / ((1 + r)^n - 1)
  // M = Monthly payment
  // P = Principal loan amount
  // r = Monthly interest rate (i.e. annual rate / 12)
  // n = Number of payments (loan term * 12)

  const principal = homePrice - downPayment;
  const monthlyRate = interestRate / 100 / 12;
  const payments = loanTerm * 12;

  const payment =
    (principal * (monthlyRate * Math.pow(1 + monthlyRate, payments))) /
    (Math.pow(1 + monthlyRate, payments) - 1);

  return `${payment.toFixed(2)} ${currency}`;
}

Vitest と統合されたユニットテストがあるので、mortgage-lib.spec.ts に任意のテストを配置して計算が正しいことを確認できます。

// packages/mortgage-lib/src/lib/mortgage-lib.spec.ts

import { calculateMortgage } from './mortgage-lib';

describe('mortgageLib', () => {
  it('calculate the monthly mortgage price for a property in USD', () => {
    const mortgage = calculateMortgage({
      homePrice: 100000,
      downPayment: 500,
      interestRate: 2.5,
      loanTerm: 30,
      currency: 'USD',
    });

    expect(mortgage).toEqual('393.15 USD');
  });
});

Nx ツールでテストを実行すると、パスしていることが分かります。

➜  banking-web-apps git:(main) ✗ nx run mortgage-lib:test

> nx run mortgage-lib:test

 RUN  v0.31.4 /Users/kevold/work/banking-web-apps/packages/mortgage-lib
 ✓ src/lib/mortgage-lib.spec.ts  (1 test) 2ms
 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  13:17:25
   Duration  1.18s (transform 60ms, setup 0ms, collect 46ms, tests 2ms, environment 449ms, prepare 104ms)

 ———————————————————————————————————————————————————————————————————————————————————————————————————————————

 >  NX   Successfully ran target test for project mortgage-lib (9s)

Nx で住宅ローン計算機コンポーネントを生成する

AWS Amplify UI ライブラリでスタイリングされた住宅ローン計算機を格納する共有 UI ライブラリを生成しましょう。

次のコマンドを実行して @nx/react をインストールし、Nx ツールを介して共有 React UI ライブラリを生成できるようにします。

npm install --save-dev @nx/react

次に、以下のコマンドを実行して共有 UI ライブラリを生成し、以下のオプションを選択します。

$ nx generate @nx/react:library shared/ui  

>  NX  Generating @nx/react:library

✔ Which stylesheet format would you like to use? · none
✔ What unit test runner should be used? · vitest
✔ Which bundler would you like to use to build the library? · vite

packages の下には、アプリケーション間で共有したい React コンポーネントのための足場となる shared/ui フォルダがあります。

packages/shared
└── ui
    ├── README.md
    ├── package.json
    ├── project.json
    ├── src
    │   ├── index.ts
    │   └── lib
    │       ├── shared-ui.spec.tsx
    │       └── shared-ui.tsx
    ├── tsconfig.json
    ├── tsconfig.lib.json
    ├── tsconfig.spec.json
    └── vite.config.ts

packages/shared/ui/src/lib/shared-ui.tsxpackages/shared/ui/src/lib/mortgageCalc.tsx に、packages/shared/ui/src/lib/shared-ui.spec.tsxpackages/shared/ui/src/lib/mortgageCalc.spec.tsx にリネームし、packages/shared/ui/src/index.ts の import を調整して、packages/shared/ui/src が以下のようになるようにします。

packages/shared
└── ui
    ├── README.md
    ├── package.json
    ├── project.json
    ├── src
    │   ├── index.ts
    │   └── lib
    │       ├── mortgageCalc.spec.tsx
    │       └── mortgageCalc.tsx
    ├── tsconfig.json
    ├── tsconfig.lib.json
    ├── tsconfig.spec.json
    └── vite.config.ts

今回のコンポーネントは Amplify UI で実装されるので、ワークスペースのルートから次のコマンドを実行して AWS Amplify UI と AWS Amplify JS ライブラリをインストールする必要があります。

npm install @aws-amplify/ui-react aws-amplify

それでは、共有ライブラリの関数 calculateMortgage を使用する住宅ローン計算機 React コンポーネント MortgageCalculator を作成しましょう。

// packages/shared/ui/src/lib/mortgageCalc.tsx
import { Button, Heading, TextField } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import { calculateMortgage } from '@banking-web-apps/mortgage-lib';
import { useState } from 'react';

export const MortgageCalculator = () => {
  const [homePrice, setHomePrice] = useState(0);
  const [downPayment, setDownPayment] = useState(0);
  const [interestRate, setInterestRate] = useState(0);
  const [loanTerm, setLoanTerm] = useState(0);
  const [currency, setCurrency] = useState('USD');
  const [monthlyPayment, setMonthlyPayment] = useState('');

  const calculate = () => {
    const payment = calculateMortgage({
      homePrice,
      downPayment,
      interestRate,
      loanTerm,
      currency,
    });
    setMonthlyPayment(payment);
  };

  return (
    <div>
      <Heading width="30vw" level={3}>
        Mortgage Calculator
      </Heading>
      <br />
      <br />
      <TextField
        label="Home price"
        type="number"
        value={homePrice}
        onChange={(e) => setHomePrice(e.target.value)}
      />
      <TextField
        label="Down payment"
        type="number"
        value={downPayment}
        onChange={(e) => setDownPayment(e.target.value)}
      />
      <TextField
        label="Interest rate"
        type="number"
        value={interestRate}
        onChange={(e) => setInterestRate(e.target.value)}
      />
      <TextField
        label="Loan term (years)"
        type="number"
        value={loanTerm}
        onChange={(e) => setLoanTerm(e.target.value)}
      />
      <br />
      <br />
      <Button onClick={calculate}>Calculate</Button>
      <br />
      <br />
      {monthlyPayment && (
        <Heading width="30vw" level={5}>
          Monthly payment: ${monthlyPayment}
        </Heading>
      )}
    </div>
  );
};

訳者注: Amplify Hosting へのデプロイ時に error TS2345: Argument of type 'string' is not assignable to parameter of type 'SetStateAction<number>' というエラーが出る場合は、e.target.valuee.target.valueAsNumber に変更してお試しください。

React Testing Library でコンポーネントのテストを統合しているので、mortgageCalc.spec.ts に任意のテストを配置して計算が正しいことを確認できます。この単純なテストは、依存関係が正しくインストールされ、コンポーネントが呼び出しアプリケーションによってレンダリングできることを保証します。

// packages/shared/ui/src/lib/mortgageCalc.spec.tsx

import { render } from '@testing-library/react';

import { MortgageCalculator } from './mortgageCalc';

describe('MortgageCalc', () => {
  it('should render successfully', () => {
    const { baseElement } = render(<MortgageCalculator />);
    expect(baseElement).toBeTruthy();
  });
});

Nx ツールでテストを実行すると、パスしていることが分かります。

$ nx run shared-ui:test  

> nx run shared-ui:test

 RUN  v0.31.4 /Users/kevold/work/banking-web-apps/packages/shared/ui
 ✓ src/lib/mortgageCalc.spec.tsx  (1 test) 99ms
 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  11:02:13
   Duration  5.28s (transform 81ms, setup 0ms, collect 2.94s, tests 99ms, environment 845ms, prepare 466ms)

 ————————————————————————————————————————————————————————————————————————————————————————————————————————————————

 >  NX   Successfully ran target test for project shared-ui (16s)

Nx で Next.js アプリ を生成する

再利用可能な住宅ローン計算関数と住宅ローン計算機コンポーネントが Nx モノレポで適切に構造化されたので、次に、例の企業 BankCorp の Bank A と Bank B の Next.js アプリを作成することに目を向けます。

まず、Next.js アプリを生成するために、npm の @nx/next パッケージが必要です。次のコマンドを実行してインストールします。

npm install --save-dev @nx/next

以下のコマンドを実行し、記載されているオプションを選択して bankA-app を生成します。

$ nx generate @nx/next:application bankA-app

>  NX  Generating @nx/next:application

✔ Which stylesheet format would you like to use? · css
✔ Would you like to use the App Router (recommended)? (Y/n) · false

BankB についても同じコマンドを実行し、packages ディレクトリをリストアップした結果は以下の通りです。Nx は Cypress を使ったエンドツーエンドのテスト用に “e2e” ディレクトリを生成しますが、この記事では、bank-a-appbank-b-app の Next.js アプリだけに焦点を当てます。

packages
├── bank-a-app
├── bank-a-app-e2e
├── bank-b-app
├── bank-b-app-e2e
├── mortgage-lib
└── shared

Next.js アプリが生成されたので、MortgageCaclulator を統合したウェブアプリを構築する準備ができました。

複数の Next.js アプリで共有コンポーネントとライブラリを使用する

この Next.js アプリでは Pages Router の設定を利用しています。bank-a-appbank-b-app アプリの両方で pages/index.tsx を開き、ヘッダーをそれぞれ “BankA” または “BankB” に置き換えて以下のようにします。

// packages/bank-a-app/pages/index.tsx

import { MortgageCalculator } from '@banking-web-apps/shared/ui';
import styles from './index.module.css';

export function Index() {
  return (
    <div className={styles.page}>
      <div className="wrapper">
        <div className="container">
          <div id="welcome">
            <h1>Welcome to BankA</h1>
          </div>

          <div id="middle-content">
            <MortgageCalculator />
          </div>
        </div>
      </div>
    </div>
  );
}

export default Index;

以下のコマンドでアプリを実行できます。

$ nx run bank-a-app:serve

> nx run bank-a-app:serve:development

- ready started server on 0.0.0.0:4200, url: http://localhost:4200

開発サーバーの URL を読み込むと、住宅ローン計算機が正しくレンダリングされ、正しい月々の支払いが計算されているのがわかります。

BankA Welcome Screen

Next.js API routes で共有ライブラリを使用する

アプリを構築するにあたって、/calculateMortgage という API route を追加し、外部開発者が自分たちのアプリでその機能を活用できるようにするという追加の要件が PM チームから出されました。

packages/bank-a-apppackages/bank-b-apppages ディレクトリ内に api フォルダを作成します。

次に、pages/api 内に calculateMortgage.ts ファイルを作成し、以下のコードを追加します。

// packages/bank-a-app/pages/api/calculateMortgage.ts

import { calculateMortgage } from '@banking-web-apps/mortgage-lib';
import { NextRequest, NextResponse } from 'next/server';

export default function handler(req: NextRequest, res: NextResponse) {
  // @ts-ignore
  const { homePrice, downPayment, interestRate, loanTerm, currency } = req.query;

  if (!homePrice || !downPayment || !interestRate || !loanTerm || !currency) {
    // @ts-ignore
    return res.json({ error: 'Missing parameters' }, { status: 400 });
  }

  const payment = calculateMortgage({
    homePrice: +homePrice,
    downPayment: +downPayment,
    interestRate: +interestRate,
    loanTerm: +loanTerm,
    currency,
  });
  
  // @ts-ignore
  res.status(200).json({ payment });
}

(まだ実行されていない場合) nx run bank-a-app:serve でアプリを起動し、GET リクエストを実行します。

GET /api/calculateMortgage?homePrice=200000&downPayment=50000&interestRate=3.5&loanTerm=30&currency=USD

支払い金額と通貨を含む以下のペイロードが返却されます。

{"payment":"673.57 USD"}

Next.js アプリを AWS Amplify Hosting にデプロイする

これで BankA と BankB の両方のアプリに住宅ローン計算機コンポーネントが表示されました。API エンドポイントとアプリを Git にコミットすれば、Amplify Hosting にデプロイする準備が完了です。

Amplify Hosting は一般的なモノレポのアプリだけでなく、npm workspace、pnpm workspace、Yarn workspace、Nx、Turborepo を使って作成されたモノレポのアプリもサポートしています。アプリをデプロイするとき、Amplify は自動的に使用しているモノレポビルドフレームワークを検出します。pnpm と Turborepo アプリは追加の設定が必要であることに注意してください。詳細は Amplify Hosting のドキュメントMonorepo build settingsDeploying a Next.js app in a monorepo を参照してください。

Amplify コンソールにアクセスし、AWS Amplify に移動します。All apps ページで、New app ドロップダウンから Host web app を選択します。

Git リポジトリのホスティングプロバイダを選択し、Continue を選択します。

Amplify Hosting Git Provider

Git プロバイダによっては、Amplify Hosting にリポジトリへのアクセスを許可するようプロンプトが表示されます。認証に成功したら Recently updated repositories list からこのアプリのリポジトリを選び、Branch で正しいブランチが選択されていることを確認し、Connecting a monorepo? Pick a folder のチェックボックスにチェックを入れ、最後にアプリのルートディレクトリのパス(この場合は packages/bank-a-app )を入力し、Next を選択します。

Amplify Hosting Add Branch

Build settings ページで、App build and test settings セクションの Edit をクリックし、preBuild コマンドを npm install に更新して Save を選択し、Next をクリックします。

Amplify Hosting Build Settings

Review ページで Save and Deploy を選択します。

Amplify Hosting Review

Amplify Hosting はプロジェクトごとに分離されたビルドとホスティング環境をプロビジョニングし、アプリをデプロイします。このプロセスには 3~4 分かかります。以下のように、ProvisionBuildDeploy のリンクを選択することで進行状況を確認できます。

Amplify Hosting Provision

Amplify Hosting Deployed

デプロイが完了したら、コンソールに表示されている URL ( https://<branch-name>.<app-id>.amplifyapp.com )にアクセスしてください。

上記の手順を繰り返し、BankB などの追加のアプリをそれぞれ別のアプリとして Amplify Hosting にデプロイします。コードが更新され、GitHub にプッシュされると、両方のアプリが新しいデプロイのためにトリガーされます。

構築したもの

この記事では、Nx を使用して共有 JavaScript ライブラリと React コンポーネントを含むリポジトリと、2 つの別々の Next.js アプリを管理し、アプリを Amplify Hosting にデプロイしました。Nx ツールを使って、モノレポ、ライブラリ、Next.js アプリの雛形を生成しました。Amplify Hosting は Nx を完全にサポートしており設定の変更は不要でしたが、今回のアプリのニーズに合わせて amplify.yml を変更しました。

翻訳は Solutions Architect の Ryotaro Tsuzuki が担当しました。