Install
npx skillscat add geckou/project-starter/new-collection Install via the SkillsCat registry.
new-collection
新しい Firestore コレクションを追加する際のチェックリスト。
手順
1. 型定義の追加
packages/shared/src/types/index.ts にドキュメントの型を追加する。
// DateLike は同じ index.ts で宣言済み(import は不要)
export type Post = {
id: string
title: string
content: string
authorId: string
createdAt: DateLike
updatedAt: DateLike
}idフィールドは必須(ドキュメント ID)- 日付フィールドは
DateLike(Date | { toDate(): Date })。書き込みはDate、
読み出しはTimestampになるため、どちらかに寄せると実行時に落ちる。
値として使うときはtoDate()(@geckou/sharedが export)を通す(→/new-type) packages/shared/src/index.tsから自動エクスポートされる
2. Firestore セキュリティルールの追加
firestore.rules にコレクションのルールを追加する。
追記位置は match /databases/{database}/documents { ... } の内側、
デフォルト拒否の match /{document=**} ブロックの後。
match /posts/{postId} {
// 認証済みユーザーは読み取り可能
allow read: if request.auth != null;
// 作成者のみ書き込み可能
allow create: if request.auth != null
&& request.resource.data.authorId == request.auth.uid;
allow update, delete: if request.auth != null
&& resource.data.authorId == request.auth.uid;
}よくあるパターン:
- 自分のデータのみ:
resource.data.userId == request.auth.uid - 認証済みなら読み取り可:
request.auth != null - 誰でも読める(公開):
allow read: if true - 管理者のみ:
request.auth.token.admin == true
注意: 1つの Firebase プロジェクトに複数環境を相乗りさせる運用では、
環境ごとに明示的な match を追加する(match /dev_posts/{postId}, match /stg_posts/{postId})。
ワイルドカード /{prefix}posts はセグメント内マッチ不可のため使えない
(firestore.rules 内のコメント参照)。
3. API エンドポイントの追加
apps/functions/src/api.ts に CRUD エンドポイントを追加する。
認証は必ず requireAuth ミドルウェア(apps/functions/src/lib/auth-middleware.ts)を使う。
エンドポイント内でトークンを手動検証しない。
リクエストボディは必ず許可フィールドのみ抽出してから書き込む。...req.body をそのままスプレッドして保存しない(authorId の偽装や余分なフィールドの混入を防ぐ)。
import { getFirestore } from 'firebase-admin/firestore'
import { requireAuth, type AuthenticatedRequest } from './lib/auth-middleware'
// GET /posts - 一覧取得
app.get('/posts', requireAuth, async (req, res) => {
const { uid } = req as AuthenticatedRequest
try {
const db = getFirestore()
const snapshot = await db
.collection('posts')
.where('authorId', '==', uid)
.orderBy('createdAt', 'desc')
.limit(20)
.get()
const posts = snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }))
// 成否は HTTP ステータスで表す。apiClient はボディをそのまま
// ApiResponse.data に入れるため、ここで包むと data.data になる
res.json(posts)
} catch {
res.status(500).json({ error: 'Internal server error' })
}
})
// POST /posts - 新規作成
app.post('/posts', requireAuth, async (req, res) => {
const { uid } = req as AuthenticatedRequest
// 許可フィールドのみ抽出してバリデーションする
const { title, content } = req.body
if (typeof title !== 'string' || title.length === 0 || typeof content !== 'string') {
res.status(400).json({ error: 'Invalid request body' })
return
}
try {
const db = getFirestore()
const docRef = await db.collection('posts').add({
title,
content,
authorId: uid,
createdAt: new Date(),
updatedAt: new Date(),
})
res.json({ id: docRef.id })
} catch {
res.status(500).json({ error: 'Internal server error' })
}
})4. 複合インデックスの追加
複合クエリ(where + orderBy の組み合わせ等)には firestore.indexes.json への
複合インデックス追加が必要。上記の一覧取得クエリの場合:
{
"collectionGroup": "posts",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "authorId", "order": "ASCENDING" },
{ "fieldPath": "createdAt", "order": "DESCENDING" }
]
}5. テストの追加
apps/functions/tests/ に API エンドポイントのテストを追加する。
Firebase Admin の getAuth() と getFirestore() をモックして検証する。
6. 確認事項
-
packages/shared/src/types/index.tsに型を追加した -
firestore.rulesにルールを追加した -
apps/functions/src/api.tsにエンドポイントを追加した - 複合クエリがある場合、
firestore.indexes.jsonに複合インデックスを追加した - テストを追加した
-
yarn type-checkが通る -
yarn testが通る