All Articles

Use Firestore with NestJS

Use Firestore With NestJS Photo by paje victoria on Unsplash

The world would be a different place if every piece of technology you can think of had an npm package that started with @nestjs/. But reality is disappointing. In this post I will show you how to use Dynamic Modules from NestJS to integrate Firestore from Google Cloud. You can use this as a template to integrate other things into NestJS.

Why do I need a module?

Sure, we can do something as simple as a tutorial, say,

const db = new Firestore({
  keyFilename: './Key.json',
  projectId: 'my-google-cloud-project',
});
const docRef = db.collections('todos').doc('firstTodo');
const myFirstTodo = (await docRef.get()).data();

but we are not here to code tutorials, are we? We need something robust and production-ready. The main issue with the code above, is that if you need to use the Firestore database somewhere else in your application, you need to instantiate a new database. Every time. This is not how NestJS works. It has the ability to instantiate things once and inject them to places where your code needs it. This is called Dependency Injection. And we can take advantage of it to use Firestore in a better way.

Just a module?

Almost. Firestore can take options to initialize. In this example, we have keyFilename (to store the auth credentials) and project (the project in Google Cloud Platform). Wouldn’t it be great to use NestJS’s built-in ConfigService to get that data from the environment variables? In the end, your App Module will look like this:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { FirestoreModule } from './firestore/firestore.module';
import { TodosModule } from './todos/todos.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    FirestoreModule.forRoot({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        keyFilename: configService.get<string>('KEY_FILE_NAME'),
        projectId: configService.get<string>('PROJECT_ID'),
      }),
      inject: [ConfigService],
    }),
    TodosModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Pretty neat, right? We are able to import the ConfigModule and inject its ConfigService to get the path to the key file and the project ID. The ConfigService will do the heavy-lifting for us to get those environment variables and their values.

Show me how!

Let’s get to it! In this article I will walk you throught the steps to install and configure Firestore in a NestJS app.

Install Firestore

Install the Firestore package from npm. We can’t go far without it.

npm install --save @google-cloud/firestore

Create the Firestore Module

This is where we will do the bulk of the work. Create a new directory in src called firestore. In it, create a new file called firestore.providers.ts. This file will have the names of the document collections. Write this code in it:

export const FirestoreDatabaseProvider = 'firestoredb';
export const FirestoreOptionsProvider = 'firestoreOptions'
export const FirestoreCollectionProviders: string[] = [];

Create a new file in the same directory called firestore.module.ts. Let’s go top-to-bottom here. First, let’s import what we will need:

import { Module, DynamicModule } from '@nestjs/common';
import { Firestore, Settings } from '@google-cloud/firestore';
import {
  FirestoreDatabaseProvider,
  FirestoreOptionsProvider,
  FirestoreCollectionProviders,
} from './firestore.providers';

We will use Module and Dynamic module to get what we want: a module for Firestore. We need Firestore to create a new Firestore instance. Settings is just an interface we need. You will soon see. And from the providers file, we import the three consts we defined in the previous file.

Second, we need a type! Remember how we need useFactory to generate those options from ConfigService? Let’s validate that return type along with other options.

type FirestoreModuleOptions = {
  imports: any[];
  useFactory: (...args: any[]) => Settings;
  inject: any[];
};

If someone else needs to use our Firestore module, this will guide them so they know what options to provide. It will give them an error if they try to pass something like cool: false as one of our options. Because, this is cool, right?

Now we can start writing our module! We need to create a special method that returns a Dynamic Module. For convention purposes, we will call this forRoot. It will take in our options defined earlier.

@Module({})
export class FirestoreModule {
  static forRoot(options: FirestoreModuleOptions): DynamicModule {
    return {};
  }
}

For now, we will return an empty object, but know that this should return a module.

If you are still with me, the file should now look like:

import { Module, DynamicModule } from '@nestjs/common';
import { Firestore, Settings } from '@google-cloud/firestore';
import {
  FirestoreDatabaseProvider,
  FirestoreOptionsProvider,
  FirestoreCollectionProviders,
} from './firestore.providers';

type FirestoreModuleOptions = {
  imports: any[];
  useFactory: (...args: any[]) => Settings;
  inject: any[];
};

@Module({})
export class FirestoreModule {
  static forRoot(options: FirestoreModuleOptions): DynamicModule {
    return {};
  }
}

Let’s start working on that forRoot method. We need options before instantiating Firestore. This will be our first provider. Insert this into the forRoot method:

const optionsProvider = {
  provide: FirestoreOptionsProvider,
  useFactory: options.useFactory,
  inject: options.inject,
};

Lots of stuff here. Provide is the “name” of this provider. How will NestJS call it when it needs to retrieve it for injection. We will use one of the constants we created. useFactory is the function that will return whatever this provider will hold. In this case, a set of options for Firestore that matches the interface Settings that we imported earlier. The function can be whatever we want, as long as it returns something of the same type as Settings. inject refers to what we need to inject to this provider for it to work. Notice that we need the ConfigService, this is where it would come in and be used in useFactory.

Now, let’s create our Firestore database provider. Write this next in our forRoot method:

const dbProvider = {
  provide: FirestoreDatabaseProvider,
  useFactory: (config) => new Firestore(config),
  inject: [FirestoreOptionsProvider],
};

provide means the same thing as before, here we use the const DATABASE_KEY. useFactory is still a function, but notice that we provided the actual function code here. This function returns a new instance of Firestore. inject is also a dependency, but the interesting thing here is that this dependency is our options created in the last provider. We use the FIRESTORE_OPTIONS_KEY constant to name it and retrieve it!

We just saved a Firestore instance into our dependencies! What if we also store collection references in the providers? Let’s try it:

const collectionProviders = FirestoreCollectionProviders.map(providerName => ({
  provide: providerName,
  useFactory: (db) => db.collection(providerName),
  inject: [FirestoreDatabaseProvider],
}));

This creates a provider for each document collection that we saved in FirestoreCollectionNames. Don’t worry, it is still an empty array.

Finally! Let’s modify that return statement and get a real Dynamic Module!

return {
  global: true,
  module: FirestoreModule,
  imports: options.imports,
  providers: [optionsProvider, dbProvider, ...collectionProviders],
  exports: [dbProvider, ...collectionProviders],
};

global decides whether our module will be global or not. module is a key containing the module name itself. This is needed for a dynamic module. imports comes from the options themselves. This could be something that our module needs for initialization (like a ConfigModule…, anyone?). providers are those that our module needs to be instantiated and exports is what we will allow other modules to use. Notice how we do not export the optionsProvider. Nobody else needs that, so let’s keep it in here only.

Alright! Our full module file will look like this, if you’ve been following along:

import { Module, DynamicModule } from '@nestjs/common';
import { Firestore, Settings } from '@google-cloud/firestore';
import {
  FirestoreDatabaseProvider,
  FirestoreOptionsProvider,
  FirestoreCollectionProviders,
} from './firestore.providers';

type FirestoreModuleOptions = {
  imports: any[];
  useFactory: (...args: any[]) => Settings;
  inject: any[];
};

@Module({})
export class FirestoreModule {
  static forRoot(options: FirestoreModuleOptions): DynamicModule {
		const optionsProvider = {
		  provide: FirestoreOptionsProvider,
		  useFactory: options.useFactory,
		  inject: options.inject,
		};
		const dbProvider = {
		  provide: FirestoreDatabaseProvider,
		  useFactory: (config) => new Firestore(config),
		  inject: [FirestoreOptionsProvider],
		};
    const collectionProviders = FirestoreCollectionProviders.map(providerName => ({
      provide: providerName,
      useFactory: (db) => db.collection(providerName),
      inject: [FirestoreDatabaseProvider],
    }));
    return {
      global: true,
      module: FirestoreModule,
      imports: options.imports,
      providers: [optionsProvider, dbProvider, ...collectionProviders],
      exports: [dbProvider, ...collectionProviders],
   };
  }
}

Import and use your new module

Now you can import your FirestoreModule. Suppose you also have a ConfigService that allows you to extract configurations from environment variables or a .env file. Your app.module.ts file would look like this:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { FirestoreModule } from './firestore/firestore.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    FirestoreModule.forRoot({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        keyFilename: configService.get<string>('SA_KEY'),
      }),
      inject: [ConfigService],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

You can see that we pass some configurations to our FirestoreModule, this includes injecting the ConfigService and importing the ConfigModule so that we are able to get the value of the SA_KEY environment variable. In this case, SA_KEY contains the path to the json key file.

Add some collections!

Let’s create a collection, which is like a table in the relational database world. According to the Firestore Documentation, a collection is created as soon as we add data to it, so all we have to do is create a reference to a collection and use it. Suppose you have a Todo module to handle your todo’s. Let’s create a directory called src/todos/documents. In it, create a new file called todo.document.ts. Stay with me, we said we needed a “collection”, but we are creating a “document”. Write this code in the new file:

import { Timestamp } from '@google-cloud/firestore';

export class TodoDocument {
  static collectionName = 'todos';

  name: string;
  dueDate: Timestamp;
}

Recall our earlier firestore.providers.ts file. Let’s open that up and add our new collection.

import { TodoDocument } from './path/to/documents/todo.document';

export const FirestoreDatabaseProvider = 'firestoredb';
export const FirestoreOptionsProvider = 'firestoreOptions'
export const FirestoreCollectionProviders: string[] = [
  TodoDocument.collectionName,
];

This will make sure that it is part of the CollectionsProvider of our Firestore Module, and therefore, injected when we start our application.

Bring it all together

Now import and inject this in our TodosService (todos.service.ts):

import {
  Injectable,
  Inject,
  Logger,
  InternalServerErrorException,
} from '@nestjs/common';
import * as dayjs from 'dayjs';
import { CollectionReference, Timestamp } from '@google-cloud/firestore';
import { TodoDocument } from './documents/todo.document';

@Injectable()
export class RawReadingsService {
  private logger: Logger = new Logger(RawReadingsService.name);

  constructor(
    @Inject(TodoDocument.collectionName)
    private todosCollection: CollectionReference<TodoDocument>,
  ) {}

  async create({ name, dueDate }): Promise<TodoDocument> {
    const docRef = this.todosCollection.doc(name);
    const dueDateMillis = dayjs(dueDate).valueOf();
    await docRef.set({
      name
      dueDate: Timestamp.fromMillis(dueDateMillis),
    });
    const todoDoc = await docRef.get();
    const todo = todoDoc.data();
    return todo;
  }

  async findAll(): Promise<TodoDocument[]> {
    const snapshot = await this.todosCollection.get();
    const todos: TodoDocument[] = [];
    snapshot.forEach(doc => todos.push(doc.data()));
    return todos;
  }
}

Or any service that needs to make use of our Firestore document. You will notice that we use CollectionReference as part of the type of our todosCollection attribute. This helps us know the type and if you use an IDE like VSCode, you are automatically offered a set of attributes and methods that you can call without needing to refer to the documentation as often.

Conclusion

We can always just import a library and use it freely in our NestJS app, but if we integrate it correctly, we can use NestJS at its best and make our code nicer and more organized along the way. I think that incorporating Firestore this way helps the developer write code faster and easier. But what about you? I encourage you to take it from here, write some code and let me know how you feel about it. Happy, and stress-free, coding. 💻

Use Firestore With NestJS With Coffee Photo by Mockup Graphics on Unsplash