Claude CodeでFirestore設計を失敗しない手順
Firestoreは「コレクションをどう分けるか」から考えると失敗しがちです。
自分も最初は users / posts / comments のようにRDBっぽく考えていました。でも実際には、「公開記事を新着順で出す」「著者ごとの記事を出す」「下書きだけ管理画面に出す」といったクエリのほうが先にあります。
この記事では、Claude Codeを使ってFirestore設計をクエリから逆算する手順をまとめます。
1. まず画面からクエリ一覧を作る
claude -p "
メディアCMSのFirestore設計をしたい。
まずコレクションを作らず、画面ごとに必要なクエリ一覧を作ってください。
where / orderBy / limit / 必要な複合インデックスも表にしてください。
"
例:
| 画面 | where | orderBy | インデックス |
|---|---|---|---|
| 記事一覧 | status == published |
publishedAt desc |
status, publishedAt |
| 著者ページ |
authorId, status
|
publishedAt desc |
authorId, status, publishedAt |
| 管理画面 | status == draft |
updatedAt desc |
status, updatedAt |
2. 一覧用の小さな情報は非正規化する
export interface PostDoc {
id: string;
slug: string;
title: string;
status: "draft" | "published" | "archived";
lang: "ja" | "en" | "es" | "ko";
authorId: string;
authorName: string;
tagSlugs: string[];
tagNames: string[];
publishedAt: FirebaseFirestore.Timestamp | null;
updatedAt: FirebaseFirestore.Timestamp;
}
authorName や tagNames は重複ですが、一覧表示で追加読み取りを減らせます。
3. Zodで保存前に検証する
import { z } from "zod";
export const CreatePostSchema = z.object({
slug: z.string().min(3).max(120).regex(/^[a-z0-9-]+$/),
title: z.string().min(1).max(120),
lang: z.enum(["ja", "en", "es", "ko"]),
authorId: z.string().min(1),
authorName: z.string().min(1),
tagSlugs: z.array(z.string()).max(8),
tagNames: z.array(z.string()).max(8),
});
4. インデックスはGit管理する
{
"indexes": [
{
"collectionGroup": "posts",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "lang", "order": "ASCENDING" },
{ "fieldPath": "status", "order": "ASCENDING" },
{ "fieldPath": "publishedAt", "order": "DESCENDING" }
]
}
]
}
5. セキュリティルールも同時に考える
match /posts/{postId} {
allow read: if resource.data.status == "published" || isAdmin();
allow create, update, delete: if isAdmin();
}
Firestoreは柔軟ですが、自由に設計してよいわけではありません。
ポイントは、クエリ・インデックス・料金・セキュリティルールを同時に見ることです。
詳しいコードと設計の考え方はブログ版にまとめています。