firebase-firestore について
TypeScriptで Firebase Firestore を利用する開発者向けの実践ガイドです。クライアント SDK と Admin SDK の操作に加え、データ処理や一般的なエラーの回避方法を扱います。
主な機能
- 01クライアント SDK と Admin SDK の初期化
- 02ドキュメント操作とリアルタイム監視
- 03複合クエリ、ページネーション、コレクショングループ
- 04バッチ書き込みとトランザクション
- 05セキュリティルール、インデックス、オフライン永続化
- 06一般的な問題の診断と予防
活用シーン
- 01Web・モバイルアプリの Firestore 操作を実装
- 02サーバー側コードで Admin SDK を設定
- 03リアルタイム、ページ分割、サブコレクション横断クエリを構築
- 04インデックス、権限、トランザクションのエラーを調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill firebase-firestoreインストール後、Claude Code で/firebase-firestoreを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonrules/firebase-firestore.mdtemplates/firebase-admin.tsfirebase-client.tsfirestore.indexes.json詳細
Firebase Firestore Database
ステータス:本番環境で利用可能 最終更新日:2026-01-25 依存関係:なし(スタンドアロンのスキル) 最新バージョン:[email protected]、[email protected]
クイックスタート(5 分)
1. Firebase SDK をインストールする
# Client SDK (web/mobile)
npm install firebase
# Admin SDK (server/backend)
npm install firebase-admin
2. Firebase を初期化する(クライアント)
// src/lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
const firebaseConfig = {
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
projectId: process.env.FIREBASE_PROJECT_ID,
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.FIREBASE_APP_ID,
};
const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
3. Firebase Admin を初期化する(サーバー)
// src/lib/firebase-admin.ts
import { initializeApp, cert, getApps } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
// Initialize only once
if (!getApps().length) {
initializeApp({
credential: cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
// Replace escaped newlines in private key
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
});
}
export const adminDb = getFirestore();
重要:
- クライアントコードで
FIREBASE_PRIVATE_KEYを公開しないでください - サーバー側の操作には Admin SDK を使用してください(セキュリティルールをバイパスします)
- 認証済みユーザーの操作には Client SDK を使用してください
主な操作
ドキュメントの CRUD(Client SDK - Modular v9+)
import {
collection,
doc,
addDoc,
getDoc,
getDocs,
setDoc,
updateDoc,
deleteDoc,
query,
where,
orderBy,
limit,
serverTimestamp,
Timestamp,
} from 'firebase/firestore';
import { db } from './firebase';
// CREATE - Auto-generated ID
const docRef = await addDoc(collection(db, 'users'), {
name: 'John Doe',
email: '[email protected]',
createdAt: serverTimestamp(),
});
console.log('Created document with ID:', docRef.id);
// CREATE - Specific ID
await setDoc(doc(db, 'users', 'user-123'), {
name: 'Jane Doe',
email: '[email protected]',
createdAt: serverTimestamp(),
});
// READ - Single document
const docSnap = await getDoc(doc(db, 'users', 'user-123'));
if (docSnap.exists()) {
console.log('Document data:', docSnap.data());
} else {
console.log('No such document!');
}
// READ - Collection with query
const q = query(
collection(db, 'users'),
where('email', '==', '[email protected]'),
orderBy('createdAt', 'desc'),
limit(10)
);
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
console.log(doc.id, ' => ', doc.data());
});
// UPDATE - Merge fields (doesn't overwrite entire document)
await updateDoc(doc(db, 'users', 'user-123'), {
name: 'Jane Smith',
updatedAt: serverTimestamp(),
});
// UPDATE - Set with merge (creates if doesn't exist)
await setDoc(doc(db, 'users', 'user-123'), {
lastLogin: serverTimestamp(),
}, { merge: true });
// DELETE
await deleteDoc(doc(db, 'users', 'user-123'));
ドキュメントの CRUD(Admin SDK)
import { adminDb } from './firebase-admin';
import { FieldValue, Timestamp } from 'firebase-admin/firestore';
// CREATE
const docRef = await adminDb.collection('users').add({
name: 'John Doe',
createdAt: FieldValue.serverTimestamp(),
});
// CREATE with specific ID
await adminDb.collection('users').doc('user-123').set({
name: 'Jane Doe',
createdAt: FieldValue.serverTimestamp(),
});
// READ
const doc = await adminDb.collection('users').doc('user-123').get();
if (doc.exists) {
console.log('Document data:', doc.data());
}
// READ with query
const snapshot = await adminDb
.collection('users')
.where('email', '==', '[email protected]')
.orderBy('createdAt', 'desc')
.limit(10)
.get();
snapshot.forEach((doc) => {
console.log(doc.id, '=>', doc.data());
});
// UPDATE
await adminDb.collection('users').doc('user-123').update({
name: 'Jane Smith',
updatedAt: FieldValue.serverTimestamp(),
});
// DELETE
await adminDb.collection('users').doc('user-123').delete();
リアルタイムリスナー(Client SDK)
import { onSnapshot, query, where, collection, doc } from 'firebase/firestore';
import { db } from './firebase';
// Listen to single document
const unsubscribe = onSnapshot(doc(db, 'users', 'user-123'), (doc) => {
if (doc.exists()) {
console.log('Current data:', doc.data());
}
});
// Listen to collection with query
const q = query(
collection(db, 'messages'),
where('roomId', '==', 'room-123'),
orderBy('createdAt', 'desc'),
limit(50)
);
const unsubscribeMessages = onSnapshot(q, (querySnapshot) => {
const messages: Message[] = [];
querySnapshot.forEach((doc) => {
messages.push({ id: doc.id, ...doc.data() } as Message);
});
// Update UI with messages
setMessages(messages);
});
// Handle errors
const unsubscribeWithError = onSnapshot(
doc(db, 'users', 'user-123'),
(doc) => {
// Handle updates
},
(error) => {
console.error('Listener error:', error);
// Handle permission denied, etc.
}
);
// IMPORTANT: Unsubscribe when done (React useEffect cleanup)
useEffect(() => {
const unsubscribe = onSnapshot(/* ... */);
return () => unsubscribe();
}, []);
重要:
- メモリリークを防ぐため、リスナーは必ず登録解除してください
- リスナーは同時接続数の上限にカウントされます
- 各リスナーは WebSocket 接続です
クエリのパターン
複合クエリ
import { query, where, orderBy, limit, startAfter, collection } from 'firebase/firestore';
// Multiple where clauses (requires composite index)
const q = query(
collection(db, 'products'),
where('category', '==', 'electronics'),
where('price', '<=', 1000),
orderBy('price', 'asc')
);
// Range query (only one field can have inequality)
const rangeQuery = query(
collection(db, 'events'),
where('date', '>=', new Date('2025-01-01')),
where('date', '<=', new Date('2025-12-31')),
orderBy('date', 'asc')
);
// Array contains
const arrayQuery = query(
collection(db, 'posts'),
where('tags', 'array-contains', 'firebase')
);
// Array contains any (max 30 values)
const arrayAnyQuery = query(
collection(db, 'posts'),
where('tags', 'array-contains-any', ['firebase', 'google', 'cloud'])
);
// In query (max 30 values)
const inQuery = query(
collection(db, 'users'),
where('status', 'in', ['active', 'pending'])
);
// Not in query (max 10 values)
const notInQuery = query(
collection(db, 'users'),
where('status', 'not-in', ['banned', 'deleted'])
);
カーソルを使用したページネーション
import { query, orderBy, limit, startAfter, getDocs, collection, DocumentSnapshot } from 'firebase/firestore';
let lastVisible: DocumentSnapshot | null = null;
async function getNextPage() {
let q = query(
collection(db, 'posts'),
orderBy('createdAt', 'desc'),
limit(10)
);
if (lastVisible) {
q = query(q, startAfter(lastVisible));
}
const snapshot = await getDocs(q);
// Save last document for next page
lastVisible = snapshot.docs[snapshot.docs.length - 1] || null;
return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}
コレクショングループクエリ
import { collectionGroup, query, where, getDocs } from 'firebase/firestore';
// Query across all subcollections named 'comments'
// Structure: posts/{postId}/comments/{commentId}
const q = query(
collectionGroup(db, 'comments'),
where('authorId', '==', 'user-123')
);
const snapshot = await getDocs(q);
// Returns all comments by user-123 across all posts
重要: コレクショングループクエリにはインデックスが必要です。Firebase Console で作成するか、firestore.indexes.json を使ってデプロイしてください。
バッチ操作とトランザクション
バッチ書き込み(最大 500 操作)
import { writeBatch, doc, collection, serverTimestamp } from 'firebase/firestore';
import { db } from './firebase';
const batch = writeBatch(db);
// Add multiple documents
const usersRef = collection(db, 'users');
batch.set(doc(usersRef), { name: 'User 1', createdAt: serverTimestamp() });
batch.set(doc(usersRef), { name: 'User 2', createdAt: serverTimestamp() });
// Update existing document
batch.update(doc(db, 'counters', 'users'), { total: 100 });
// Delete document
batch.delete(doc(db, 'temp', 'old-doc'));
// Commit all operations atomically
await batch.commit();
トランザクション(読み取り後に書き込み)
import { runTransaction, doc, increment } from 'firebase/firestore';
import { db } from './firebase';
// Transfer credits between users
async function transferCredits(fromId: string, toId: string, amount: number) {
await runTransaction(db, async (transaction) => {
const fromRef = doc(db, 'users', fromId);
const toRef = doc(db, 'users', toId);
const fromDoc = await transaction.get(fromRef);
const toDoc = await transaction.get(toRef);
if (!fromDoc.exists() || !toDoc.exists()) {
throw new Error('User not found');
}
const fromCredits = fromDoc.data().credits;
if (fromCredits < amount) {
throw new Error('Insufficient credits');
}
transaction.update(fromRef, { credits: fromCredits - amount });
transaction.update(toRef, { credits: increment(amount) });
});
}
重要:
- トランザクションは失敗し、自動的に再試行される場合があります(最大 5 回)
- トランザクション内で副作用を伴う処理を実行しないでください(複数回実行される可能性があります)
- トランザクションでは、すべての読み取りをすべての書き込みより先に行う必要があります
セキュリティルール
基本的なルールの構造
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper functions
function isAuthenticated() {
return request.auth != null;
}
function isOwner(userId) {
return request.auth.uid == userId;
}
function isValidUser() {
return request.resource.data.keys().hasAll(['name', 'email'])
&& request.resource.data.name is string
&& request.resource.data.email is string;
}
// Users collection
match /users/{userId} {
allow read: if isAuthenticated();
allow create: if isAuthenticated() && isOwner(userId) && isValidUser();
allow update: if isOwner(userId);
allow delete: if isOwner(userId);
}
// Posts collection with subcollections
match /posts/{postId} {
allow read: if resource.data.published == true || isOwner(resource.data.authorId);
allow create: if isAuthenticated() && request.resource.data.authorId == request.auth.uid;
allow update, delete: if isOwner(resource.data.authorId);
// Comments subcollection
match /comments/{commentId} {
allow read: if true;
allow create: if isAuthenticated();
allow update, delete: if isOwner(resource.data.authorId);
}
}
// Admin-only collection
match /admin/{document=**} {
allow read, write: if request.auth.token.admin == true;
}
}
}
ルールをデプロイする
# Deploy rules
firebase deploy --only firestore:rules
# Deploy rules and indexes
firebase deploy --only firestore
インデックス
複合インデックス(firestore.indexes.json)
{
"indexes": [
{
"collectionGroup": "products",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "category", "order": "ASCENDING" },
{ "fieldPath": "price", "order": "ASCENDING" }
]
},
{
"collectionGroup": "comments",
"queryScope": "COLLECTION_GROUP",
"fields": [
{ "fieldPath": "authorId", "order": "ASCENDING" },
{ "fieldPath": "createdAt", "order": "DESCENDING" }
]
}
],
"fieldOverrides": []
}
# Deploy indexes
firebase deploy --only firestore:indexes
重要:
- Firestore は単一フィールドのインデックスを自動作成します
- 複合インデックスは手動で作成するか、エラー内のリンクから作成する必要があります
- コレクショングループクエリには常にインデックスが必要です
オフライン永続化
オフラインサポートを有効にする(Web)
import { initializeFirestore, persistentLocalCache, persistentMultipleTabManager } from 'firebase/firestore';
import { app } from './firebase';
// Enable multi-tab offline persistence
const db = initializeFirestore(app, {
localCache: persistentLocalCache({
tabManager: persistentMultipleTabManager()
})
});
// OR: Enable single-tab persistence (simpler)
import { enableIndexedDbPersistence, getFirestore } from 'firebase/firestore';
const db = getFirestore(app);
enableIndexedDbPersistence(db).catch((err) => {
if (err.code === 'failed-precondition') {
// Multiple tabs open, persistence can only be enabled in one tab
console.warn('Persistence failed: multiple tabs open');
} else if (err.code === 'unimplemented') {
// Browser doesn't support persistence
console.warn('Persistence not supported');
}
});
オフライン状態を処理する
import { onSnapshot, doc, SnapshotMetadata } from 'firebase/firestore';
onSnapshot(doc(db, 'users', 'user-123'), (doc) => {
const source = doc.metadata.fromCache ? 'local cache' : 'server';
console.log(`Data came from ${source}`);
if (doc.metadata.hasPendingWrites) {
console.log('Local changes pending sync');
}
});
データモデリングのベストプラクティス
読み取りパフォーマンスを高めるための非正規化
// Instead of joining users and posts...
// Store author info directly in post document
// posts/{postId}
{
title: 'My Post',
content: '...',
authorId: 'user-123',
// Denormalized author data for fast reads
author: {
name: 'John Doe',
avatarUrl: 'https://...'
},
createdAt: Timestamp
}
サブコレクションとルートコレクション
// Subcollections: Good for parent-child relationships
// posts/{postId}/comments/{commentId}
// - Easy to query all comments for a post
// - Deleting post doesn't auto-delete comments (use Cloud Functions)
// Root collections: Good for cross-cutting queries
// comments (with postId field)
// - Easy to query all comments by a user across posts
// - Requires manual data consistency
カウンターパターン(書き込み頻度の高いシナリオ)
// Direct increment (low traffic)
await updateDoc(doc(db, 'posts', postId), {
viewCount: increment(1)
});
// Distributed counter (high traffic - 1000+ writes/sec)
// Use Cloud Functions to aggregate shard counts
// counters/{counterId}/shards/{shardId}
エラー処理
よくあるエラーと解決策
| エラー | 原因 | 解決策 |
|---|---|---|
permission-denied | セキュリティルールによってアクセスがブロックされている | ルールを確認し、ユーザーが認証済みであることを確認する |
not-found | ドキュメントが存在しない | データにアクセスする前に exists() で確認する |
already-exists | 同じ ID のドキュメントがすでに存在する | setDoc で merge を使用するか、新しい ID を生成する |
resource-exhausted | クォータを超過している | プランをアップグレードするか、クエリを最適化する |
failed-precondition | クエリに必要なインデックスがない | 複合インデックスを作成する(エラー内のリンクを参照) |
unavailable | サービスが一時的に利用できない | バックオフを伴う再試行を実装する |
invalid-argument | 無効なクエリの組み合わせ | クエリの制約を確認する(以下を参照) |
deadline-exceeded | 操作がタイムアウトした | データサイズを減らすか、ページネーションを使用する |
クエリの制約
// INVALID: Multiple inequality filters on different fields
query(collection(db, 'posts'),
where('date', '>', startDate),
where('likes', '>', 100) // ERROR: Can't use inequality on second field
);
// VALID: Use range on one field, equality on others
query(collection(db, 'posts'),
where('category', '==', 'tech'),
where('date', '>', startDate)
);
// INVALID: orderBy field different from inequality field
query(collection(db, 'posts'),
where('date', '>', startDate),
orderBy('likes') // ERROR: Must orderBy('date') first
);
// VALID: orderBy inequality field first
query(collection(db, 'posts'),
where('date', '>', startDate),
orderBy('date'),
orderBy('likes')
);
既知の問題の防止
このスキルでは、文書化されている Firestore のエラー 10 件を防止します。
| Issue # | エラー/問題 | 説明 | 回避方法 | 出典 |
|---|---|---|---|---|
| #1 | permission-denied | セキュリティルールによって操作がブロックされる | まず Firebase Console エミュレーターでルールをテストする | 一般的 |
| #2 | failed-precondition(インデックス) | 複合インデックスが存在しない | エラー内のリンクをクリックしてインデックスを作成するか、firestore.indexes.json で定義する | 一般的 |
| #3 | クエリの組み合わせが無効 | 複数の不等号フィルター | 不等号フィルターは 1 つのフィールドだけに使用し、ほかのフィールドには等号を使用する | ドキュメント |
| #4 | リスナーによるメモリリーク | onSnapshot の購読解除が行われない | クリーンアップ時(useEffect の戻り値)に必ず unsubscribe を呼び出す | 一般的 |
| #5 | オフライン永続化の競合 | 複数のタブで永続化を使用している | persistentMultipleTabManager() を使用するか、エラーを処理する | ドキュメント |
| #6 | トランザクションの副作用 | 副作用が複数回実行される | runTransaction 内では副作用を実行しない | ドキュメント |
| #7 | バッチの上限超過 | 操作が 500 件を超える | 複数のバッチに分割する | ドキュメント |
| #8 | resource-exhausted | クォータ制限に達した | ページネーションを実装し、読み取りを減らし、キャッシュを使用する | 一般的 |
| #9 | 秘密鍵の改行に関する問題 | 環境変数内の \\n が変換されない | 秘密鍵に .replace(/\\n/g, '\n') を使用する | 一般的 |
| #10 | コレクショングループクエリの失敗 | コレクショングループインデックスが存在しない | queryScope: COLLECTION_GROUP を指定してインデックスを作成する | ドキュメント |
Firebase CLI コマンド
# Initialize Firestore
firebase init firestore
# Start emulators
firebase emulators:start --only firestore
# Deploy rules and indexes
firebase deploy --only firestore
# Export data (for backup)
gcloud firestore export gs://your-bucket/backups/$(date +%Y%m%d)
# Import data
gcloud firestore import gs://your-bucket/backups/20250125
パッケージのバージョン(2026-01-25 確認済み)
{
"dependencies": {
"firebase": "^12.8.0"
},
"devDependencies": {
"firebase-admin": "^13.6.0"
}
}
公式ドキュメント
- Firestore の概要:https://firebase.google.com/docs/firestore
- はじめに:https://firebase.google.com/docs/firestore/quickstart
- データモデル:https://firebase.google.com/docs/firestore/data-model
- セキュリティルール:https://firebase.google.com/docs/firestore/security/get-started
- データのクエリ:https://firebase.google.com/docs/firestore/query-data/queries
- データの管理:https://firebase.google.com/docs/firestore/manage-data/add-data
- オフラインデータ:https://firebase.google.com/docs/firestore/manage-data/enable-offline
最終確認日:2026-01-25 | スキルのバージョン:1.0.0
コメント
Markdown 対応