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.tsx
を packages/shared/ui/src/lib/mortgageCalc.tsx
に、packages/shared/ui/src/lib/shared-ui.spec.tsx
を packages/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.value
を e.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-app
と bank-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-app
と bank-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 を読み込むと、住宅ローン計算機が正しくレンダリングされ、正しい月々の支払いが計算されているのがわかります。
Next.js API routes で共有ライブラリを使用する
アプリを構築するにあたって、/calculateMortgage
という API route を追加し、外部開発者が自分たちのアプリでその機能を活用できるようにするという追加の要件が PM チームから出されました。
packages/bank-a-app
と packages/bank-b-app
の pages
ディレクトリ内に 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¤cy=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 settings と Deploying a Next.js app in a monorepo を参照してください。
Amplify コンソールにアクセスし、AWS Amplify に移動します。All apps ページで、New app ドロップダウンから Host web app を選択します。
Git リポジトリのホスティングプロバイダを選択し、Continue を選択します。
Git プロバイダによっては、Amplify Hosting にリポジトリへのアクセスを許可するようプロンプトが表示されます。認証に成功したら Recently updated repositories list からこのアプリのリポジトリを選び、Branch で正しいブランチが選択されていることを確認し、Connecting a monorepo? Pick a folder のチェックボックスにチェックを入れ、最後にアプリのルートディレクトリのパス(この場合は packages/bank-a-app
)を入力し、Next を選択します。
Build settings ページで、App build and test settings セクションの Edit をクリックし、preBuild コマンドを npm install
に更新して Save を選択し、Next をクリックします。
Review ページで Save and Deploy を選択します。
Amplify Hosting はプロジェクトごとに分離されたビルドとホスティング環境をプロビジョニングし、アプリをデプロイします。このプロセスには 3~4 分かかります。以下のように、Provision、Build、Deploy のリンクを選択することで進行状況を確認できます。
デプロイが完了したら、コンソールに表示されている 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 が担当しました。