Building a Generic Firestore Service in Angular

Ekrem Koçak
5 min readNov 9, 2024

--

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”.
Firebase Project 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.

GitHub Repository

StackBlitz

--

--

Responses (1)