Overview
Hello everyone! π
In this article, I'll walk you through everything you need to know about Firestore - Google's flagship NoSQL document database. If you've been wondering how to store and manage data in your web applications without dealing with complex SQL queries and server management, you're in the right place!
Firestore is honestly a game-changer. It's real-time, scalable, and incredibly developer-friendly. Plus, it integrates seamlessly with all Firebase services and works beautifully across web, mobile, and server environments.
Let's start! π€
What Makes Firestore Special?
Before we jump into the code, let's understand why Firestore is so awesome:
Real-time Updates: Your app updates instantly when data changes - no polling needed!
Offline Support: Works offline and syncs when connection returns
Scalability: Automatically scales to handle millions of users
Security: Built-in security rules to protect your data
Multi-platform: Same API across web, iOS, Android, and server
Flexible Queries: Rich querying capabilities with compound queries
Unlike traditional SQL databases, Firestore is a NoSQL document database. Think of it as storing JSON objects that can contain other JSON objects. Pretty neat, right? π
Understanding Firestore Structure
Firestore organizes data in a simple hierarchy:
Database
βββ Collection (e.g., "users")
βββ Document (e.g., "user123")
βββ Fields (e.g., name, email, age)
βββ Subcollection (e.g., "posts")
βββ Document (e.g., "post456")
βββ Fields (e.g., title, content)
Documents: Like JSON objects with key-value pairs
Collections: Groups of documents (like tables in SQL)
Subcollections: Collections inside documents for nested data
Here's what a user document might look like:
{
"name": "John Doe",
"email": "john@example.com",
"age": 30,
"createdAt": "2024-01-15T10:30:00Z",
"preferences": {
"theme": "dark",
"notifications": true
}
}
Pretty straightforward! π―
Setup Firebase Project
Before we dive into the code, let's set up our Firebase project.
To setup the project, you can retrieve this article where I talk about it.
Enable Firestore in Firebase Console
- Go to your Firebase project console
- Click on "Firestore Database"
- Click "Create database"
- Choose "Start in test mode" (for development)
- Select a location close to your users
Your database is now ready! π
Basic CRUD Operations
Let's learn the fundamental operations: Create, Read, Update, and Delete.
Step 1: Writing Data (Create)
Here's how to add documents to Firestore:
import { db } from './firebase';
import { collection, addDoc, doc, setDoc } from 'firebase/firestore';
// Add a document with auto-generated ID
const addUser = async (userData) => {
try {
const docRef = await addDoc(collection(db, 'users'), userData);
console.log('Document written with ID: ', docRef.id);
return docRef.id;
} catch (error) {
console.error('Error adding document: ', error);
}
};
// Add a document with custom ID
const addUserWithId = async (userId, userData) => {
try {
await setDoc(doc(db, 'users', userId), userData);
console.log('Document written with custom ID: ', userId);
} catch (error) {
console.error('Error adding document: ', error);
}
};
// Usage
await addUser({
name: 'Jane Doe',
email: 'jane@example.com',
age: 28,
createdAt: new Date()
});
Step 2: Reading Data (Read)
Here's how to fetch data from Firestore:
import { doc, getDoc, collection, getDocs, query, where } from 'firebase/firestore';
// Get a single document
const getUser = async (userId) => {
const docRef = doc(db, 'users', userId);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
console.log('Document data:', docSnap.data());
return docSnap.data();
} else {
console.log('No such document!');
return null;
}
};
// Get all documents in a collection
const getAllUsers = async () => {
const querySnapshot = await getDocs(collection(db, 'users'));
const users = [];
querySnapshot.forEach((doc) => {
users.push({ id: doc.id, ...doc.data() });
});
return users;
};
// Get documents with conditions
const getAdultUsers = async () => {
const q = query(collection(db, 'users'), where('age', '>=', 18));
const querySnapshot = await getDocs(q);
const adults = [];
querySnapshot.forEach((doc) => {
adults.push({ id: doc.id, ...doc.data() });
});
return adults;
};
Step 3: Updating Data (Update)
Here's how to update existing documents:
import { doc, updateDoc, setDoc } from 'firebase/firestore';
// Update specific fields
const updateUser = async (userId, updates) => {
const userRef = doc(db, 'users', userId);
try {
await updateDoc(userRef, updates);
console.log('Document updated successfully');
} catch (error) {
console.error('Error updating document: ', error);
}
};
// Usage
await updateUser('user123', {
age: 31,
'preferences.theme': 'light' // Update nested field
});
// Replace entire document
const replaceUser = async (userId, userData) => {
await setDoc(doc(db, 'users', userId), userData);
};
Step 4: Deleting Data (Delete)
Here's how to remove documents:
import { doc, deleteDoc } from 'firebase/firestore';
// Delete a document
const deleteUser = async (userId) => {
try {
await deleteDoc(doc(db, 'users', userId));
console.log('Document deleted successfully');
} catch (error) {
console.error('Error deleting document: ', error);
}
};
// Usage
await deleteUser('user123');
In this repo, there is a simple example of CRUD operation.
Advanced Querying
Firestore supports powerful queries. Let's explore them! π
Step 1: Simple Queries
import { collection, query, where, orderBy, limit, startAfter, getDocs } from 'firebase/firestore';
// Basic where clause
const getActiveUsers = async () => {
const q = query(
collection(db, 'users'),
where('status', '==', 'active')
);
return await getDocs(q);
};
// Multiple conditions
const getYoungActiveUsers = async () => {
const q = query(
collection(db, 'users'),
where('status', '==', 'active'),
where('age', '<', 30)
);
return await getDocs(q);
};
// Array contains
const getUsersWithSkill = async (skill) => {
const q = query(
collection(db, 'users'),
where('skills', 'array-contains', skill)
);
return await getDocs(q);
};
Step 2: Ordering and Limiting
// Order by field
const getUsersByAge = async () => {
const q = query(
collection(db, 'users'),
orderBy('age', 'desc')
);
return await getDocs(q);
};
// Limit results
const getRecentUsers = async () => {
const q = query(
collection(db, 'users'),
orderBy('createdAt', 'desc'),
limit(10)
);
return await getDocs(q);
};
// Pagination
const getUsersPage = async (lastDoc) => {
const q = query(
collection(db, 'users'),
orderBy('createdAt', 'desc'),
startAfter(lastDoc),
limit(10)
);
return await getDocs(q);
};
Step 3: Compound Queries
// Complex query with multiple conditions
const searchUsers = async (minAge, maxAge, status) => {
const q = query(
collection(db, 'users'),
where('age', '>=', minAge),
where('age', '<=', maxAge),
where('status', '==', status),
orderBy('age'),
limit(20)
);
return await getDocs(q);
};
Working with Subcollections
Subcollections are perfect for hierarchical data. Let's see how to use them! ποΈ
Step 1: Creating Subcollections
import { collection, addDoc, doc } from 'firebase/firestore';
// Add a post to a user's posts subcollection
const addUserPost = async (userId, postData) => {
const userPostsRef = collection(db, 'users', userId, 'posts');
try {
const docRef = await addDoc(userPostsRef, {
...postData,
createdAt: new Date(),
authorId: userId
});
console.log('Post added with ID: ', docRef.id);
return docRef.id;
} catch (error) {
console.error('Error adding post: ', error);
}
};
// Usage
await addUserPost('user123', {
title: 'My First Post',
content: 'This is the content of my first post!',
tags: ['firebase', 'javascript', 'tutorial']
});
Step 2: Querying Subcollections
import { collection, getDocs, query, orderBy } from 'firebase/firestore';
// Get all posts from a specific user
const getUserPosts = async (userId) => {
const postsRef = collection(db, 'users', userId, 'posts');
const q = query(postsRef, orderBy('createdAt', 'desc'));
const querySnapshot = await getDocs(q);
const posts = [];
querySnapshot.forEach((doc) => {
posts.push({ id: doc.id, ...doc.data() });
});
return posts;
};
// Get posts from all users (collection group query)
import { collectionGroup } from 'firebase/firestore';
const getAllPosts = async () => {
const q = query(collectionGroup(db, 'posts'), orderBy('createdAt', 'desc'));
const querySnapshot = await getDocs(q);
const allPosts = [];
querySnapshot.forEach((doc) => {
allPosts.push({ id: doc.id, ...doc.data() });
});
return allPosts;
};
Security Rules Deep Dive
Security rules are crucial for protecting your data. Let's set them up properly! π‘οΈ
Step 1: Basic Rules Structure
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Rules go here
}
}
Step 2: User-specific Rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users can only access their own data
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Users can only access their own posts
match /users/{userId}/posts/{postId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
Step 3: Public and Private Data
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Public posts - anyone can read, only author can write
match /posts/{postId} {
allow read: if true;
allow write: if request.auth != null &&
request.auth.uid == resource.data.authorId;
}
// Private user data
match /users/{userId}/private/{document=**} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Public user profiles
match /users/{userId}/public/{document=**} {
allow read: if true;
allow write: if request.auth != null && request.auth.uid == userId;
}
}
}
Step 4: Custom Functions in Rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper function
function isOwner(userId) {
return request.auth != null && request.auth.uid == userId;
}
function isAdmin() {
return request.auth != null &&
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}
match /posts/{postId} {
allow read: if true;
allow write: if isOwner(resource.data.authorId) || isAdmin();
allow delete: if isOwner(resource.data.authorId) || isAdmin();
}
}
}
Performance Optimization
Let's make sure your Firestore queries are lightning fast! β‘
Step 1: Create Indexes
Firestore automatically creates single-field indexes, but you need composite indexes for complex queries:
// This query needs a composite index
const complexQuery = query(
collection(db, 'posts'),
where('status', '==', 'published'),
where('category', '==', 'tech'),
orderBy('createdAt', 'desc')
);
Firebase will suggest creating indexes when you run queries that need them.
Step 2: Optimize Document Size
Keep documents under 1MB for best performance:
// β
Good document size
const user = {
name: "John",
email: "john@example.com",
preferences: { theme: "dark" }
};
// β Document too large
const badUser = {
name: "John",
email: "john@example.com",
allMessages: [/* thousands of messages */] // Use subcollection instead!
};
Step 3: Use Pagination
Don't load all data at once:
const loadPostsPage = async (pageSize = 10, lastDoc = null) => {
let q = query(
collection(db, 'posts'),
orderBy('createdAt', 'desc'),
limit(pageSize)
);
if (lastDoc) {
q = query(q, startAfter(lastDoc));
}
const snapshot = await getDocs(q);
const posts = [];
let newLastDoc = null;
snapshot.forEach((doc) => {
posts.push({ id: doc.id, ...doc.data() });
newLastDoc = doc; // Save for next page
});
return { posts, lastDoc: newLastDoc, hasMore: posts.length === pageSize };
};
Error Handling and Best Practices
Let's make your Firestore code robust and production-ready! ποΈ
Step 1: Proper Error Handling
const safeAddUser = async (userData) => {
try {
const docRef = await addDoc(collection(db, 'users'), userData);
return { success: true, id: docRef.id, error: null };
} catch (error) {
console.error('Error adding user:', error);
return { success: false, id: null, error: error.message };
}
};
// Usage
const result = await safeAddUser(newUser);
if (result.success) {
console.log('User created:', result.id);
} else {
console.error('Failed to create user:', result.error);
}
Step 2: Loading States and UI Feedback
// React example with loading states
const UserList = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const loadUsers = async () => {
try {
setLoading(true);
const snapshot = await getDocs(collection(db, 'users'));
const userList = [];
snapshot.forEach((doc) => {
userList.push({ id: doc.id, ...doc.data() });
});
setUsers(userList);
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
loadUsers();
}, []);
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
Step 3: Cleanup and Memory Management
// Always cleanup listeners
useEffect(() => {
const unsubscribe = onSnapshot(
collection(db, 'posts'),
(snapshot) => {
// Handle updates
}
);
// Cleanup on unmount
return () => unsubscribe();
}, []);
// Batch your reads when possible
const getBatchData = async (userIds) => {
const promises = userIds.map(id => getDoc(doc(db, 'users', id)));
const docs = await Promise.all(promises);
return docs.map(doc =>
doc.exists() ? { id: doc.id, ...doc.data() } : null
).filter(Boolean);
};
Deployment and Production Tips
Ready to take your Firestore app to production? Here are the essentials! π
Step 1: Production Security Rules
Update your security rules for production:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Production-ready rules
match /users/{userId} {
allow read: if request.auth != null;
allow write: if request.auth != null && request.auth.uid == userId;
}
match /posts/{postId} {
allow read: if resource.data.published == true;
allow create: if request.auth != null &&
request.auth.uid == request.resource.data.authorId;
allow update, delete: if request.auth != null &&
request.auth.uid == resource.data.authorId;
}
// Admin-only collections
match /admin/{document=**} {
allow read, write: if request.auth != null &&
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}
}
}
Step 2: Set Up Indexes
Deploy your indexes with Firebase CLI:
firebase deploy --only firestore:indexes
Step 3: Monitor Performance
Set up monitoring to track:
- Read/write operations
- Error rates
- Query performance
- Security rule violations
Go to Firebase Console β Firestore β Usage tab to see these metrics.
Step 4: Environment Configuration
Use different Firebase projects for different environments:
// config.js
const configs = {
development: {
projectId: "my-app-dev",
// dev config
},
production: {
projectId: "my-app-prod",
// prod config
}
};
const config = configs[process.env.NODE_ENV || 'development'];
Conclusion
Firestore really is a fantastic database. It handles scaling, real-time updates, offline support, and security for you, so you can focus on building great user experiences instead of managing database infrastructure.
The learning curve might seem steep at first, but once you get the hang of the document-based approach, you'll wonder how you ever lived without it. Start small, experiment with the examples in this article, and gradually build more complex features as you get comfortable.
Happy coding!β¨
Hiππ»
My name is Domenico, software developer passionate of Open Source, I write article about it for share my knowledge and experience.
Don't forget to visit my Linktree to discover my projects π«°π»
Linktree: https://linktr.ee/domenicotenace
Follow me on dev.to for other articles ππ»
If you like my content or want to support my work on GitHub, you can support me with a very small donation.
I would be grateful π₯Ή
Top comments (0)