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:
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:
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
);
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 );
// 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:
Soft Delete a Document
Documents are marked with deletedAt instead of being removed: await userRepo . softDelete ( 'user-123' );
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)
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
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:
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