ECサイトって、1店舗だけならそこまで難しく考えなくても作れます。
商品一覧があって、詳細があって、カートに入れて、購入する。
ただ、同じコードで複数ショップを動かしたいとなると、急に考えることが増えました。
最初は shopId みたいな値で分岐すれば良さそうと思っていたんですが、
ドメイン、商品取得、メニュー、テーマ、404、テストまで絡んでくると、
ああ、これは早めに境界を決めないと後でつらそうだなと。。。
まず分けたかったもの
一番最初に気になったのは、URL とテナントの対応です。
同じアプリケーションでも、アクセスされたドメインによって表示するショップが変わる。
ここを各ページで毎回考えると、たぶんすぐ壊れます。
なので、まずはこういう考え方に寄せました。
interface TenantConfig {
tenantId: string;
shopName: string;
theme: ThemeConfig;
commerce: CommerceConfig;
}
const domainToTenant: Record<string, string> = {
'shop-a.example.com': 'shop-a',
'shop-b.example.com': 'shop-b'
};
function resolveTenant(domain: string): TenantConfig | null {
const tenantId = domainToTenant[domain];
if (!tenantId) {
return null;
}
return tenants[tenantId] ?? null;
}
コードとしては普通なんですが、
「ドメインからテナントを決める」と「テナントから表示設定を取る」を分けておくのが大事そうでした。
ここを混ぜると、あとで開発用ドメイン、確認用ドメイン、本番ドメインが増えた時にしんどい。
header で見るか、host で見るか
Next.js だと middleware や server component 側で request header を見られます。
なので、テナント解決は request の入口に寄せたい。
function getTenantDomain(headers: Headers): string {
return (
headers.get('x-tenant-domain') ??
headers.get('host') ??
''
);
}
ローカルやE2Eだと、実ドメインを毎回用意するのがめんどくさいです。
なので、テストでは header でテナントを差し替えられると便利でした。
本番は host を見る。
テストやプレビューは x-tenant-domain みたいなヘッダーで差し替える。
この逃げ道を作っておくと、開発がかなり楽になります。
unknown tenant はちゃんと落とす
意外と大事なのが、知らないドメインでアクセスされた時です。
なんとなく default tenant を出すと、一見便利なんですが、
テナント設定ミスに気づきにくくなります。
const tenant = resolveTenant(domain);
if (!tenant) {
notFound();
}
ここは最初から 404 にした方がいいと思いました。
設定が足りていないなら、早く壊れてくれた方がいい。
ECだと別ショップの商品や表示が混ざる方が怖いので、
「見つからなかったら適当に出す」は避けたいです。
1店舗ならいらない抽象化
正直、1店舗だけならここまで分けなくていいです。
設定ファイルもいらないし、ドメイン解決もいらない。
ただ、2店舗、3店舗と増える前提があるなら、
最初にこのくらいの境界は作っておいた方が良さそうでした。
- ドメインからテナントを解決する
- テナントごとの設定を1箇所に寄せる
- unknown tenant は 404 にする
- テストでは header でテナントを切り替えられるようにする
このあたり。
全部を汎用化しようとすると重いけど、
毎回 if (shopId === '...') を足す構成にはしたくない。
おわり
最初は「ドメインで出し分けるだけでしょ」と思っていました。
でも実際に触っていくと、テナント解決はその後の設計全部に効いてくる感じがありました。
商品取得、テーマ、メニュー、テスト。
全部がここから始まる。
今なら、マルチテナントECを作る時は、最初にページを作るより先に、
「このリクエストはどのショップのものか」を安全に決めるところから始めると思います。