Skip to main content

Quick Start Guide

This guide will walk you through creating your first FirestoreORM repository and performing basic CRUD operations.

Complete Working Example

Let’s build a user management system from scratch.

Step 1: Initialize Firebase

First, set up your Firebase Admin SDK connection:
config/firebase.ts
import { initializeApp, cert } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';

const app = initializeApp({
  credential: cert('./serviceAccountKey.json')
});

export const db = getFirestore(app);
Make sure you’ve downloaded your Firebase service account key and placed it in your project root. See the Installation Guide for details.

Step 2: Define Your Schema

Create a Zod schema to validate your data:
schemas/user.schema.ts
import { z } from 'zod';

export const userSchema = z.object({
  id: z.string().optional(),
  name: z.string().min(1, 'Name is required'),
  email: z.string().email('Invalid email address'),
  age: z.number().int().positive().optional(),
  status: z.enum(['active', 'inactive', 'suspended']).default('active'),
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime()
});

export type User = z.infer<typeof userSchema>;
Always include id: z.string().optional() in your schema. The ORM will auto-generate IDs for new documents.

Step 3: Create Your Repository

Create a repository instance for the users collection:
repositories/user.repository.ts
import { FirestoreRepository } from '@spacelabstech/firestoreorm';
import { db } from '../config/firebase';
import { userSchema, User } from '../schemas/user.schema';

export const userRepo = FirestoreRepository.withSchema<User>(
  db,
  'users',
  userSchema
);

Step 4: Perform CRUD Operations

Now you can use the repository to interact with Firestore:
import { userRepo } from './repositories/user.repository';

// Create a new user
const user = await userRepo.create({
  name: 'John Doe',
  email: '[email protected]',
  age: 30,
  status: 'active',
  createdAt: new Date().toISOString(),
  updatedAt: new Date().toISOString()
});

console.log('Created user:', user.id);
// Output: Created user: abc123def456

Querying Data

FirestoreORM provides a powerful query builder for complex queries:

Basic Queries

// Find active users
const activeUsers = await userRepo.query()
  .where('status', '==', 'active')
  .get();

console.log(`Found ${activeUsers.length} active users`);

Advanced Queries

// Complex query with multiple conditions
const results = await userRepo.query()
  .where('status', '==', 'active')
  .where('age', '>', 18)
  .orderBy('createdAt', 'desc')
  .limit(10)
  .get();

console.log('Recent adult users:', results);

Pagination

// Cursor-based pagination (recommended)
const { items, nextCursorId } = await userRepo.query()
  .orderBy('createdAt', 'desc')
  .paginate(20);

console.log('First page:', items);

// Get next page
if (nextCursorId) {
  const nextPage = await userRepo.query()
    .orderBy('createdAt', 'desc')
    .paginate(20, nextCursorId);
  
  console.log('Second page:', nextPage.items);
}

Soft Deletes and Recovery

FirestoreORM includes built-in soft delete functionality:
1

Soft Delete a Document

Documents are marked with deletedAt instead of being removed:
await userRepo.softDelete('user-123');
2

Soft Deleted Documents are Hidden

By default, soft-deleted documents don’t appear in queries:
const user = await userRepo.getById('user-123');
console.log(user); // null (even though document still exists)
3

Access Deleted Documents

Use includeDeleted flag to retrieve soft-deleted documents:
const deletedUser = await userRepo.getById('user-123', true);
console.log(deletedUser.deletedAt); // ISO timestamp
4

Restore Deleted Documents

Recover soft-deleted documents easily:
await userRepo.restore('user-123');
const restored = await userRepo.getById('user-123');
console.log('User restored:', restored.name);
Soft deletes only work when you use repository methods. Direct Firestore SDK calls bypass this functionality.

Validation and Error Handling

FirestoreORM automatically validates data against your Zod schema:
import { ValidationError, NotFoundError } from '@spacelabstech/firestoreorm';

try {
  // This will fail validation
  await userRepo.create({
    name: '', // Too short
    email: 'not-an-email', // Invalid format
    age: -5, // Must be positive
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString()
  });
} catch (error) {
  if (error instanceof ValidationError) {
    console.log('Validation failed:');
    error.issues.forEach(issue => {
      console.log(`  ${issue.path.join('.')}: ${issue.message}`);
    });
    // Output:
    // Validation failed:
    //   name: String must contain at least 1 character(s)
    //   email: Invalid email address
    //   age: Number must be greater than 0
  }
}

Handling Not Found Errors

try {
  await userRepo.update('non-existent-id', {
    name: 'New Name',
    updatedAt: new Date().toISOString()
  });
} catch (error) {
  if (error instanceof NotFoundError) {
    console.log('User not found');
  }
}

Bulk Operations

Perform operations on multiple documents efficiently:

Bulk Create

const users = await userRepo.bulkCreate([
  {
    name: 'Alice',
    email: '[email protected]',
    status: 'active',
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString()
  },
  {
    name: 'Bob',
    email: '[email protected]',
    status: 'active',
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString()
  },
  {
    name: 'Charlie',
    email: '[email protected]',
    status: 'active',
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString()
  }
]);

console.log(`Created ${users.length} users`);

Bulk Update

await userRepo.bulkUpdate([
  { id: 'user-1', data: { status: 'active' } },
  { id: 'user-2', data: { status: 'inactive' } },
  { id: 'user-3', data: { status: 'suspended' } }
]);

Query-Based Updates

// Update all matching documents
const updatedCount = await userRepo.query()
  .where('status', '==', 'inactive')
  .update({
    status: 'active',
    updatedAt: new Date().toISOString()
  });

console.log(`Updated ${updatedCount} users`);

Nested Field Updates with Dot Notation

Update nested fields without replacing entire objects:
// Define schema with nested objects
const userSchema = z.object({
  id: z.string().optional(),
  name: z.string(),
  address: z.object({
    street: z.string(),
    city: z.string(),
    zipCode: z.string()
  }),
  settings: z.object({
    notifications: z.boolean(),
    theme: z.string()
  })
});

// Update only specific nested fields
await userRepo.update('user-123', {
  'address.city': 'Los Angeles',
  'settings.theme': 'dark'
} as any);

// Other fields (address.street, address.zipCode, settings.notifications) remain unchanged
The as any cast is required for dot notation keys since they’re dynamic strings. The update is still validated against your schema.

Lifecycle Hooks

Add custom logic at specific points in the data lifecycle:
import { userRepo } from './repositories/user.repository';

// Log all user creations
userRepo.on('afterCreate', async (user) => {
  console.log(`New user created: ${user.id}`);
});

// Send welcome email
userRepo.on('afterCreate', async (user) => {
  await sendWelcomeEmail(user.email);
});

// Validate business rules before update
userRepo.on('beforeUpdate', (data) => {
  if (data.status === 'suspended' && !data.suspendedReason) {
    throw new Error('Suspended users must have a reason');
  }
});

// Now create a user - hooks will run automatically
const user = await userRepo.create({
  name: 'Jane Doe',
  email: '[email protected]',
  status: 'active',
  createdAt: new Date().toISOString(),
  updatedAt: new Date().toISOString()
});
// Console: "New user created: xyz789"
// Email sent to [email protected]

Complete Example: User Management API

Here’s a complete example combining everything:
app.ts
import { initializeApp, cert } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
import { FirestoreRepository, ValidationError, NotFoundError } from '@spacelabstech/firestoreorm';
import { z } from 'zod';

// 1. Initialize Firebase
const app = initializeApp({
  credential: cert('./serviceAccountKey.json')
});
const db = getFirestore(app);

// 2. Define schema
const userSchema = z.object({
  id: z.string().optional(),
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
  status: z.enum(['active', 'inactive', 'suspended']).default('active'),
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime()
});

type User = z.infer<typeof userSchema>;

// 3. Create repository
const userRepo = FirestoreRepository.withSchema<User>(
  db,
  'users',
  userSchema
);

// 4. Add hooks
userRepo.on('afterCreate', async (user) => {
  console.log(`✓ User ${user.name} created with ID: ${user.id}`);
});

// 5. Use the repository
async function main() {
  try {
    // Create user
    const user = await userRepo.create({
      name: 'John Doe',
      email: '[email protected]',
      age: 30,
      status: 'active',
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    });

    // Query users
    const activeUsers = await userRepo.query()
      .where('status', '==', 'active')
      .where('age', '>', 18)
      .orderBy('createdAt', 'desc')
      .limit(10)
      .get();

    console.log(`Found ${activeUsers.length} active adult users`);

    // Update user
    await userRepo.update(user.id, {
      age: 31,
      updatedAt: new Date().toISOString()
    });

    // Soft delete
    await userRepo.softDelete(user.id);
    console.log('User soft deleted (can be restored)');

    // Restore
    await userRepo.restore(user.id);
    console.log('User restored');

  } catch (error) {
    if (error instanceof ValidationError) {
      console.error('Validation failed:', error.message);
    } else if (error instanceof NotFoundError) {
      console.error('Document not found:', error.message);
    } else {
      console.error('Error:', error);
    }
  }
}

main();

Next Steps

Now that you understand the basics, explore more advanced features:

Query Builder

Learn about advanced queries, pagination, and aggregations

Transactions

Perform atomic operations with ACID guarantees

Lifecycle Hooks

Add logging, validation, and side effects to your data layer

Error Handling

Handle validation, not found, and Firestore index errors

Build docs developers (and LLMs) love