DEV Community

Cover image for Firestore Database: Your NoSQL Best Friend πŸ”₯
Domenico Tenace for This is Learning

Posted on

Firestore Database: Your NoSQL Best Friend πŸ”₯

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)
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Go to your Firebase project console
  2. Click on "Firestore Database"
  3. Click "Create database"
  4. Choose "Start in test mode" (for development)
  5. 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()
});
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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']
});
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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')
);
Enter fullscreen mode Exit fullscreen mode

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!
};
Enter fullscreen mode Exit fullscreen mode

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 };
};
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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';
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Set Up Indexes

Deploy your indexes with Firebase CLI:

firebase deploy --only firestore:indexes
Enter fullscreen mode Exit fullscreen mode

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'];
Enter fullscreen mode Exit fullscreen mode

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 πŸ₯Ή

Buy Me A Coffee

Top comments (0)