Building a Generic Firestore Service in Angular
Introduction
In this article, we will create a generic Firestore service using Angular and Firebase. This approach allows us to manage Firestore CRUD operations in a centralized way, reducing code repetition and making our Angular app more maintainable.
Let’s get started!
1. Introduction to Angular, Firebase, and Firestore
Angular is a popular frontend framework developed by Google. It is used to build large-scale, dynamic web applications with a component-based architecture.
Firebase is a platform from Google that offers various backend services like authentication, real-time databases, and cloud storage.
Firestore is a NoSQL cloud database that provides flexible, real-time data synchronization. It organizes data into collections and documents.
2. Creating a New Angular Project
First, create a new Angular project:
ng new generic-firestore-service
This command will create a project named generic-firestore-service
.
3. Setting Up Firebase Project
- Go to Firebase Console.
- Click “Crate a project” and follow the steps to create a new project.
4. Creating a Firestore Database
- Go to “Build” > “Firestore Database”.
- Click “Create Database”.
- Choose “Start in test mode” to allow easy access during development.
5. Installing Firebase CLI
To manage Firebase services, install the Firebase CLI globally:
npm install -g firebase-tools
6. Adding Firebase to Angular Project
Integrate Firebase with your Angular project using the following commands:
npm install @angular/fire
ng add @angular/fire
This will install necessary dependencies and set up Firebase configuration.
After completing all these steps, your app.config.ts
file should look like this.
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { getFirestore, provideFirestore } from '@angular/fire/firestore';
import { environment } from './environments/environment';
import { FIREBASE_OPTIONS } from '@angular/fire/compat';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideFirebaseApp(() => initializeApp(environment.firebaseOptions)),
{ provide: FIREBASE_OPTIONS, useValue: environment.firebaseOptions },
provideFirestore(() => getFirestore()),
],
};
7. Configuring Firebase
If the configuration is not added automatically, you can add it manually from Firebase Console:
- Go to “Project Settings” > SDK Setup And Configuration” > “Config”.
- Copy the configuration and update your
environment.ts
file:
export const environment = {
production: false,
firebaseOptions: {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT_ID.appspot.com",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID"
}
};
8. Creating a Generic Firestore Service
Create a generic Firestore service to manage CRUD operations:
ng generate service firestore
import { Injectable, inject } from '@angular/core';
import { AngularFirestore, QueryFn } from '@angular/fire/compat/firestore';
import { from, map } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export abstract class FireStoreService<T extends { id?: string }> {
protected readonly firestore = inject(AngularFirestore);
protected readonly collectionName: string;
constructor(collection: string) {
this.collectionName = collection;
}
collection(queryFn?: QueryFn) {
return this.firestore.collection<T>(this.collectionName, queryFn);
}
getSnapshotChanges(queryFn?: QueryFn) {
return this.firestore
.collection<T>(this.collectionName, queryFn)
.snapshotChanges()
.pipe(
map((data) =>
data.map((d) => {
const data: T = { id: d.payload.doc.id, ...d.payload.doc.data() };
return data;
})
)
);
}
add(data: T) {
return from(this.firestore.collection<T>(this.collectionName).add(data));
}
update(data: T) {
return from(
this.firestore
.collection<T>(this.collectionName)
.doc(data.id)
.update(data)
);
}
delete(data: T) {
return from(
this.firestore.collection<T>(this.collectionName).doc(data.id).delete()
);
}
}
This service will handle common Firestore tasks like adding, deleting, and updating documents.
9. Creating a Todo Service
Next, we create a specific service for Todo items that extends our generic Firestore service:
ng generate service todo
import { Injectable } from '@angular/core';
import { FireStoreService } from '../firestore.service';
export interface Todo {
id?: string;
name: string;
isDone: boolean;
createdTime: number;
}
@Injectable({ providedIn: 'root' })
export class TodoService extends FireStoreService<Todo> {
constructor() {
super('todos');
}
}
This service will be used to manage a Todo list.
10. Creating a Todo Component
Create a component to display and manage the Todo items:
ng generate component components/todo
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { take } from 'rxjs';
import { Todo, TodoService } from './todo.service';
@Component({
selector: 'app-todo-list',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, FormsModule],
templateUrl: './todo-list.component.html',
})
export class TodoListComponent {
todo: Todo = { name: '', isDone: false, createdTime: Date.now() };
private readonly todoService = inject(TodoService);
todos$ = this.todoService.getSnapshotChanges((ref) =>
ref.orderBy('createdTime', 'desc')
);
async submit() {
this.todoService
.add(this.todo)
.pipe(take(1))
.subscribe({
next: () => {
console.warn('success');
this.todo = { ...this.todo, name: '', isDone: false };
},
error: () => {
console.error('error');
},
});
}
updateTodo(todo: Todo) {
this.todoService.update(todo);
}
deleteTodo(todo: Todo) {
this.todoService.delete(todo);
}
}
<input type="text" [(ngModel)]="todo.name" />
<button [disabled]="!todo.name.length" (click)="submit()">Add +</button>
@for (todo of todos$ | async; track $index) {
<div style="display: flex; padding: 1rem 0">
<span [innerText]="todo.name"></span>
<div>
<input
type="checkbox"
[(ngModel)]="todo.isDone"
(ngModelChange)="updateTodo(todo)"
/>
<button (click)="deleteTodo(todo)">X</button>
</div>
</div>
}
11. Running the Application
Run the Angular application with:
ng serve -o
This command will open the app in your default browser.
12. Conclusion
In this tutorial, we built a reusable generic Firestore service in Angular using Firebase. This service can be extended to manage various data models in a clean and efficient way. By centralizing Firestore operations, we reduce code duplication and make our application easier to maintain.
This setup allows you to quickly integrate and use Firebase Firestore in your Angular projects, making it easier to manage different data types with minimal code changes.