マルチテナントECを作っていて、地味に気になったのが fetch の場所です。
商品一覧や商品詳細は、そのページで取ればいい。
これは分かりやすいです。
でも、ヘッダーのメニューやショップ共通の情報はどうするのか。
全ページで使うから layout で取る。
最初はそれで良さそうに見えました。
ただ、テナントが増える前提だと、ここが少し怖い。
layout で取ると便利
例えば、全ページ共通のメニューを layout で取るとします。
export default async function RootLayout({ children }: Props) {
const tenant = await getTenant();
const menu = await fetchMenu(tenant);
return (
<TenantProvider tenant={tenant}>
<Header menu={menu} />
{children}
</TenantProvider>
);
}
これ自体はきれいです。
どのページでも Header が出る。
ページ側はメニューを気にしなくていい。
ただ、全リクエストで fetchMenu が走る可能性があります。
ショップごとに外部APIを叩くなら、ここはちゃんと考えないといけない。
何が怖いか
1店舗なら、多少雑でも気づけます。
でも複数ショップになると、
- Aショップだけメニュー取得が遅い
- Bショップだけ設定が足りない
- Cショップだけ外部APIのレスポンスが違う
- 開発環境だけドメイン解決が違う
みたいなことが起きます。
しかも layout で落ちると、どのページも落ちます。
これはなかなか痛い。
cache するか fallback するか
メニューみたいな情報は、毎回リアルタイムでなくても良いことが多いです。
なので、今なら最初から cache 前提で考えます。
async function getTenantMenu(tenant: TenantConfig): Promise<MenuItem[]> {
const cached = await menuCache.get(tenant.id);
if (cached) {
return cached;
}
const menu = await fetchMenu(tenant);
await menuCache.set(tenant.id, menu);
return menu;
}
実装は Next.js の fetch cache でも、KV でも、アプリ内 cache でもいいと思います。
大事なのは、
「layout に置いた fetch は全ページに影響する」
と分かっておくこと。
あと、取得に失敗した時に全部落とすのか、最低限のメニューで出すのかも決めたい。
try {
return await fetchMenu(tenant);
} catch (error) {
console.error(error);
return tenant.fallbackMenu;
}
ECなので、購入導線に関わるところは雑に fallback しない方がいいです。
でも、ヘッダーの補助メニューくらいなら fallback した方が良い場面もありそう。
ここは機能ごとに分けて考えたいです。
E2E が1テナントだけだと危ない
もうひとつ気になったのがテストです。
E2E が1つのショップだけを見ていると、
別のショップの設定ミスに気づけません。
test('top page renders', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading')).toBeVisible();
});
これだと、どの tenant で見ているかが曖昧です。
テナントを header で切り替えられるなら、最低限こうしたい。
for (const tenantDomain of tenantDomains) {
test(`${tenantDomain} top page renders`, async ({ browser }) => {
const context = await browser.newContext({
extraHTTPHeaders: {
'x-tenant-domain': tenantDomain
}
});
const page = await context.newPage();
await page.goto('/');
await expect(page.getByRole('main')).toBeVisible();
});
}
全部の画面を全部のテナントで見る必要はないと思います。
でも、
- top が出る
- menu が出る
- 商品一覧が出る
- unknown tenant は 404 になる
このくらいは全テナントで見たい。
テスト環境の罠
ローカルの E2E は、別のアプリが同じ port で起動していても通ったように見えることがあります。
これが地味に怖い。
localhost:3000 に何かがいる。
Playwright はそこを見に行く。
でも、それが今テストしたいアプリとは限らない。
ああ、そんなことあるんだなと。。。
なので、E2E の前にアプリの identity を確認するのは大事そうです。
await page.goto('/');
await expect(page.locator('[data-app="ec-app"]')).toBeVisible();
こういう目印でもいいし、
特定の tenant 名が出ることでもいい。
「ブラウザが開けた」だけでは信用しない。
おわり
複数ショップ対応は、画面を出すだけならそこまで難しくないです。
でも、増えてきた時に効いてくるのは、
- 共通 layout で何を fetch するか
- どこを cache するか
- 失敗時に fallback するか
- E2E が全テナントを最低限見ているか
- そもそも正しいアプリをテストしているか
このあたりでした。
昔なら「表示できた、OK」で終わっていたと思います。
今は、表示できた後に、
「この fetch は全ページで走るけど大丈夫か」
「テナントが増えた時にテストで気づけるか」
まで見たい。
マルチテナントECは、実装よりも増えた時の確認方法が大事なのかもしれないです。