Firebase Firestore Skill
Create production-ready Firestore database queries with TypeScript, proper type lifecycle, structured error handling, and best practices.
Quick Reference
| Document | Purpose |
|---|---|
| types.md | Type utilities and lifecycle patterns |
| errors.md | DbError class and error categorization |
| config.md | Firebase Admin SDK configuration |
| examples.md | Complete CRUD operation examples |
| patterns.md | Best practices and anti-patterns |
| seeding.md | Database seeding patterns |
Instructions
Before Implementation
- •Read project context at
.claude/project-context.mdfor project-specific patterns - •Check existing patterns in
lib/firebase/for configuration - •Review feature structure in
features/*/server/db/for query patterns
Workflow
- •Define types using the type lifecycle pattern (Base → Firestore → Application → DTOs)
- •Create transform function to convert Firestore data to application types
- •Implement queries with tuple return pattern
[DbError | null, Data | null] - •Add error handling using
DbErrorclass andcategorizeDbError - •Include logging with structured context at all error points
- •Test edge cases (empty inputs, not found, permissions)
File Organization
code
features/
└── [feature]/
├── types/
│ └── [resource].ts # Type definitions
└── server/
└── db/
├── [resource]-queries.ts # CRUD operations
└── seed-[resource].ts # Seeding (if needed)
Core Concepts
Type Lifecycle
Firebase requires different type representations at different stages:
typescript
// 1. Base type - Business fields only
type ResourceBase = {
name: string;
status: "active" | "inactive";
};
// 2. Firestore type - With Timestamp objects
type ResourceFirestore = WithFirestoreTimestamps<ResourceBase>;
// 3. Application type - With id and Date objects
type Resource = WithDates<ResourceBase>;
// 4. Create DTO - For creating documents
type CreateResourceDto = CreateDto<ResourceBase>;
// 5. Update DTO - For updating documents
type UpdateResourceDto = UpdateDto<ResourceBase>;
See types.md for complete type utilities.
Error Handling Pattern
All database queries return tuples for explicit error handling:
typescript
export async function getResourceById(
id: string
): Promise<[null, Resource] | [DbError, null]> {
// Input validation
if (!id?.trim()) {
return [DbError.validation("Invalid id provided"), null];
}
try {
const doc = await db.collection(COLLECTION).doc(id).get();
if (!doc.exists) {
return [DbError.notFound(RESOURCE_NAME), null];
}
return [null, transformToResource(doc.id, doc.data()!)];
} catch (error) {
return [categorizeDbError(error, RESOURCE_NAME), null];
}
}
See errors.md for complete error handling.
Transform Functions
Convert Firestore documents to application types:
typescript
function transformToResource(
docId: string,
data: FirebaseFirestore.DocumentData
): Resource {
return {
id: docId,
...data,
// Convert Timestamp to Date
createdAt: data.createdAt?.toDate(),
updatedAt: data.updatedAt?.toDate(),
} as Resource;
}
Questions to Ask
Before implementing database queries, clarify:
- •What is the resource name? (e.g., "Budget", "User", "Category")
- •What fields does the resource have? (business fields only)
- •Are there custom Date fields? (beyond createdAt/updatedAt)
- •What queries are needed? (getById, getAll, getByUserId, etc.)
- •Is seeding required? (predefined data)
- •What are the access patterns? (by user, by status, etc.)
Usage Examples
Basic Query Function
typescript
import "server-only";
import { db } from "~/lib/firebase";
import { categorizeDbError, DbError } from "~/lib/firebase/errors";
import { createLogger } from "~/lib/logger";
import type { Budget } from "../types/budget";
const logger = createLogger({ module: "budget-db" });
const COLLECTION = "budgets";
const RESOURCE = "Budget";
export async function getBudgetById(
id: string
): Promise<[null, Budget] | [DbError, null]> {
if (!id?.trim()) {
const error = DbError.validation("Invalid budget id");
logger.warn({ errorCode: error.code }, "Validation failed");
return [error, null];
}
try {
const doc = await db.collection(COLLECTION).doc(id).get();
if (!doc.exists) {
logger.warn({ budgetId: id }, "Budget not found");
return [DbError.notFound(RESOURCE), null];
}
logger.info({ budgetId: id }, "Budget retrieved");
return [null, transformToBudget(doc.id, doc.data()!)];
} catch (error) {
const dbError = categorizeDbError(error, RESOURCE);
logger.error({ budgetId: id, errorCode: dbError.code }, "Query failed");
return [dbError, null];
}
}
Usage in Server Actions
typescript
export async function updateBudget(
budgetId: string,
data: UpdateBudgetDto
): ActionResponse<Budget> {
const { userId } = await auth();
if (!userId) {
return { success: false, error: "Unauthorized" };
}
const [error, budget] = await updateBudgetInDb(budgetId, data);
if (error) {
if (error.isNotFound) {
return { success: false, error: "Budget not found" };
}
return { success: false, error: error.message };
}
revalidatePath("/budgets");
return { success: true, data: budget };
}
Usage in Page Loaders
typescript
async function loadBudget(budgetId: string) {
const { userId } = await auth();
if (!userId) redirect("/sign-in");
const [error, budget] = await getBudgetById(budgetId);
if (error) {
if (error.isNotFound) notFound();
if (error.isRetryable) throw error; // Let error.tsx handle
throw new Error("Unable to load budget");
}
return budget;
}
Related Documentation
- •types.md - Complete type utilities
- •errors.md - Error handling patterns
- •config.md - Firebase configuration
- •examples.md - Full CRUD examples
- •patterns.md - Best practices
- •seeding.md - Seeding patterns
Related Skills
- •
server-actions- For implementing server actions that use these queries - •
db-migration- For migrating Firestore data