Host a NestJS App on Firebase Functions: The Complete Guide

A step-by-step guide on hosting a NestJS application on Firebase Functions, using Firebase Authentication and Firestore.

Robert Isaac
11 min readAug 22, 2022

In this article, I will give you all the info you need to make an API with NestJS and Firebase, which includes hosting the NestJS application on Firebase Functions, using Firebase Authentication and Firestore so let’s get started.

Note: I assume in this article you already know NestJS and Firebase (but only basic knowledge needed to understand the article), so I won’t be explaining any of them in details. This article was written with intent to help you connect both together in the best way possible.

Here are the steps we will go through:

1. Create the project
2. Add Firebase Functions
3. Deploy to Firebase functions
4. Add Firebase Authentication
5. Enable CORS
6. Connect NestJS with Firebase Admin
7. Enhance our scripts for a better developer experience
8. Connect with Firestore

1. Create the project

The first thing is that you need to install the global package of NestJS and Firebase by running:

npm install -g firebase-tools @nestjs/cli

Then create your project:

nest new todo

2. Add Firebase Functions

First of all, you need to create a project in Firebase from https://console.firebase.google.com/ then you need to change your plan to be Blaze (otherwise, Firebase functions won’t work), from the left sidebar menu click on functions then click get started till you finish the wizard

Then go to the project and execute the following commands:

firebase login
firebase init functions

Then select Use an existing project and select the project you just have created.

Then continue.

? What language would you like to use to write Cloud Functions? TypeScript
? Do you want to use ESLint to catch probable bugs and enforce style? No
+ Wrote functions/package.json
+ Wrote functions/tsconfig.json
+ Wrote functions/src/index.ts
+ Wrote functions/.gitignore
? Do you want to install dependencies with npm now? No

You will find a new folder created called functions inside of it the files mentioned above.

We only care about the package.json inside it, we need to merge it with the main package.json we have.

To do so we will copy all scripts from functions/package.json to our main package.json except build and start since they are already there.

Then copy engines and main properties and all dependencies except for TypeScript (since it’s already in the main package.json).

Then delete the functions folder and create a new file index.ts in the root of the project with this content:

import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import * as express from 'express';
import * as functions from 'firebase-functions';
import { AppModule } from './src/app.module';const expressServer = express();const createFunction = async (expressInstance): Promise<void> => {
const app = await NestFactory.create(
AppModule,
new ExpressAdapter(expressInstance),
);
await app.init();
};
export const api = functions.https.onRequest(async (request, response) => {
await createFunction(expressServer);
expressServer(request, response);
});

Then change the main in the package.json to point to dist/index.js instead of lib/index.js.

So, package.json should look like this:

{
"name": "todo",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"serve": "npm run build && firebase emulators:start --only functions",
"shell": "npm run build && firebase functions:shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "14"
},
"main": "dist/index.js",
"dependencies": {
"@nestjs/common": "^8.0.0",
"@nestjs/core": "^8.0.0",
"@nestjs/platform-express": "^8.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"firebase-admin": "^9.8.0",
"firebase-functions": "^3.14.1"
},
"devDependencies": {
"@nestjs/cli": "^8.0.0",
"@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.0.0",
"@types/express": "^4.17.13",
"@types/jest": "^26.0.24",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
"eslint": "^7.30.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"jest": "27.0.6",
"prettier": "^2.3.2",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.10.1",
"typescript": "^4.3.5",
"firebase-functions-test": "^0.2.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

But please don’t copy and paste this, as this can have outdated dependencies and/or some other changes from NestJS or Firebase, we need to merge the two package.json that were generated.

Last but not least modify firebase.json file because by default it looks for the functions folder, so we need to make it "source": "." this means look at the root folder instead of functions one, it will look like this:

{
"functions": {
"predeploy": "npm --prefix \"$RESOURCE_DIR\" run build",
"source": "."
}
}

Now you need to run:

npm run serve

If all is good then you will get a long message with one of them like:

functions[us-central1-api]: http function initialized (http://localhost:5001/todo-7b299/us-central1/api).

Open this URL in the browser to verify it’s actually working, it should say Hello World!

3. Deploying to Firebase Functions

We need to run:

npm run deploy

If all is good it should give you a long message with one line:

Function URL (api(us-central1)): https://us-central1-todo-7b299.cloudfunctions.net/api

If you faced an error it can be because the engine you choose is not supported by Firebase, or that you haven’t run npm i after you changed the package.json , or that your plan is not Blaze.

If you opened this URL it may give you:

Error: Forbidden
Your client does not have permission to get URL /api from this server.

That’s because it’s not public yet, to make it public you need to do the following:

  1. Go to Firebase console https://console.firebase.google.com/.
  2. Select your project.
  3. Go to ‘functions’ tab.
  4. Select the three dots beside the function name and choose Detailed usage stats , this will take you to Google Cloud.
  5. Go to ‘permission’ tab and add permission.
  6. Choose ‘allUsers’ principal and Cloud function invoker rule as shown in the below image and save.
make function publicly accessed

Now refresh the URL and it should also give you “Hello World!” this time.

And congratulations! You have deployed your NestJS app to Firebase Functions.

4. Add Firebase Authentication

Note: You need to enable how users will authenticate for this to work but usually this is part of the frontend/mobile.

Let’s start by creating a todo resource using:

nest g res todo

And let’s cleanup the project a bit by removing app.controller.ts and app.service.ts and their mention in the app.module.ts.

Then we need to make all of todo routes accessible only with a Firebase JWT token because now if you go to api/todo it will show you “This action returns all todo” while it shows only return if it’s an authenticated request.

First, let’s install all required dependencies:

npm i @nestjs/passport passport passport-firebase-jwt

Then let’s create a global module called auth with:

nest g mo auth

And add @Global() before @Module to make it accessible to all of our modules.

Then inside the auth folder create a new file firebase-auth.strategy.ts with the following content:

import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { Strategy, ExtractJwt } from 'passport-firebase-jwt';
import { auth } from 'firebase-admin';

@Injectable()
export class FirebaseAuthStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
});
}

validate(token) {
return auth()
.verifyIdToken(token, true)
.catch((err) => {
console.warn(err);
throw new UnauthorizedException();
});
}
}

And then use it in the auth.module.ts, to look like the following:

import { Global, Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { FirebaseAuthStrategy } from './firebase-auth.strategy';

@Global()
@Module({
imports: [PassportModule.register({ defaultStrategy: 'firebase-jwt' })],
providers: [FirebaseAuthStrategy],
exports: [PassportModule],
})
export class AuthModule {}

Then finally add @UseGuards(AuthGuard()) in the TodoController.

Now when you go to http://localhost:5001/todo-7b299/us-central1/api/todo it should return 401 (but you need to restart the server).

5. Enable CORS

Now let’s start testing our API.

I have created a very simple Angular project to test the API with.

Let’s clone it, modify the environment.ts and see if our API will work or not.

The first issue you will face is the CORS errors.

To enable it simply in the index.ts file add app.enableCors().

To be like that:

const createFunction = async (expressInstance): Promise<void> => {
const app = await NestFactory.create(
AppModule,
new ExpressAdapter(expressInstance),
);
app.enableCors();
app.useGlobalPipes(new ValidationPipe());
await app.init();
};

And please restart the npm run serve after you have added that.

You will find that the CORS error disappeared, but a new error is thrown:

[Nest] 16468  - 08/06/2021, 10:51:49 AM   ERROR [ExceptionsHandler] The default Firebase app does not exist. Make sure you call initializeApp() before using any of the Firebase services.

6. Connect NestJS with Firebase Admin

Now the issue is that NestJS can’t connect yet with Firebase, it works only on deploying the code.

There are many ways to connect it with Firebase, but I prefer to use Firebase Config. Thus the credentials aren’t in the code which makes it safer, also it works on both local and the cloud without having to provide them in multiple ways.

So the first step is to download the credentials file.

You will do like this screenshot:

This will download a JSON file.

Now you need to encode it, simply using JSON.stringify.

Before:

{
"type": "service_account",
"project_id": "todo-7b299",
"private_key_id": "foobar",
"private_key": "-----BEGIN PRIVATE KEY-----...",
"client_email": "something.iam.gserviceaccount.com",
"client_id": "123",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-foo.iam.gserviceaccount.com"
}

After:

"{\"type\":\"service_account\",\"project_id\":\"todo-7b299\",\"private_key_id\":\"foobar\",\"private_key\":\"-----BEGIN PRIVATE KEY-----...\",\"client_email\":\"something.iam.gserviceaccount.com\",\"client_id\":\"123\",\"auth_uri\":\"https://accounts.google.com/o/oauth2/auth\",\"token_uri\":\"https://oauth2.googleapis.com/token\",\"auth_provider_x509_cert_url\":\"https://www.googleapis.com/oauth2/v1/certs\",\"client_x509_cert_url\":\"https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-foo.iam.gserviceaccount.com\"}"

Now you can execute this command:

firebase functions:config:set todo_firebase_config="{\"type\":\"service_account\",\"project_id\":\"todo-7b299\",\"private_key_id\":\"foobar\",\"private_key\":\"-----BEGIN PRIVATE KEY-----...\",\"client_email\":\"something.iam.gserviceaccount.com\",\"client_id\":\"123\",\"auth_uri\":\"https://accounts.google.com/o/oauth2/auth\",\"token_uri\":\"https://oauth2.googleapis.com/token\",\"auth_provider_x509_cert_url\":\"https://www.googleapis.com/oauth2/v1/certs\",\"client_x509_cert_url\":\"https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-foo.iam.gserviceaccount.com\"}"

To verify if it’s working, run firebase functions:config:get.

$ firebase functions:config:get
{
"todo_firebase_config": {
"project_id": "todo-7b299",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-foo.iam.gserviceaccount.com",
"token_uri": "https://oauth2.googleapis.com/token",
"private_key": "-----BEGIN PRIVATE KEY-----...",
"client_email": "something.iam.gserviceaccount.com",
"type": "service_account",
"private_key_id": "foobar",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_id": "123"
}
}

Now let’s load this by modifying index.ts and adding:

admin.initializeApp({
credential: admin.credential.cert(functions.config().todo_firebase_config),
databaseURL: 'https://todo-c9f6e-default-rtdb.firebaseio.com',
});

Now that would work fine in the cloud, but locally it’s still the same error, to fix it we need to run a command for the Functions Emulator to work properly, and that is:

firebase functions:config:get > .runtimeconfig.json

That would return the content of the functions:config:get into a file called .runtimeconfig.json.

So we need to add this file in the .gitignore.

7. Enhance our scripts for better developer experience

So now we should run this command before every time we start, i.e., in case a colleague changed something, or someone cloned the project for first time.

And there is another issue now, which is the need to restart our application every time we are making a change.

So we need to have a better scripts to handle all of this for us.

For that I’m using the concurrently package. Install it as a dev using the following:

npm i -D concurrently

Then change the scripts as shown below:

"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"build:watch": "nest build --watch",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"get:config": "firebase functions:config:get > .runtimeconfig.json",
"firebase:emulator": "firebase emulators:start --only functions",
"serve": "npm run get:config && npm run build && npm run firebase:emulator",
"serve:watch": "concurrently npm:get:config npm:build && concurrently npm:build:watch npm:firebase:emulator",
"shell": "npm run build && firebase functions:shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
}

Now you only need to run the following:

npm run serve:watch

And it will handle everything for you, and no more need to restart the application.

8. Connect with Firestore

The final step is to connect our application with Firestore to store our data.

Before we start you must create the database in the Firebase console.

First thing is to define our entity and DTO

todo.entity.ts

export class Todo {
id: string;
title: string;
details: string;
userId: string;
createdAt: string;
}

create-todo.dto.ts

import { IsNotEmpty, IsString } from 'class-validator';

export class CreateTodoDto {
@IsNotEmpty()
@IsString()
title: string;

@IsString()
details: string;
}

In order for the validation to work we need to install class-validator and class-transformer dependencies and add app.useGlobalPipes(new ValidationPipe()); to our index.ts file.

Now we need to connect our service to firebase this is the full code:

import { Inject, Injectable } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
import { firestore } from 'firebase-admin';
import { REQUEST } from '@nestjs/core';
import { Todo } from './entities/todo.entity';
import DocumentSnapshot = firestore.DocumentSnapshot;
import QuerySnapshot = firestore.QuerySnapshot;

@Injectable()
export class TodoService {
private collection: FirebaseFirestore.CollectionReference<FirebaseFirestore.DocumentData>;

constructor(@Inject(REQUEST) private readonly request: { user: any }) {
this.collection = firestore().collection('todos');
}

async create(createTodoDto: CreateTodoDto) {
const userId = this.request.user.uid;
const todo: Omit<Todo, 'id'> = {
...createTodoDto,
createdAt: new Date().toISOString(),
userId,
};

return this.collection.add(todo).then((doc) => {
return { id: doc.id, ...todo };
});
}

findAll() {
return this.collection
.where('userId', '==', this.request.user.uid)
.get()
.then((querySnapshot: QuerySnapshot<Todo>) => {
if (querySnapshot.empty) {
return [];
}

const todos: Todo[] = [];
for (const doc of querySnapshot.docs) {
todos.push(this.transformTodo(doc));
}

return todos;
});
}

findOne(id: string) {
return this.collection
.doc(id)
.get()
.then((querySnapshot: DocumentSnapshot<Todo>) => {
return this.transformTodo(querySnapshot);
});
}

async update(id: string, updateTodoDto: UpdateTodoDto) {
await this.collection.doc(id).update(updateTodoDto);
}

async remove(id: string) {
await this.collection.doc(id).delete();
}

private transformTodo(querySnapshot: DocumentSnapshot<Todo>) {
if (!querySnapshot.exists) {
throw new Error(`no todo found with the given id`);
}

const todo = querySnapshot.data();
const userId = this.request.user.uid;

if (todo.userId !== userId) {
throw new Error(`no todo found with the given id`);
}

return {
id: querySnapshot.id,
...todo,
};
}
}

Then in our controller we need to only remove casting the id from string to number:

import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { TodoService } from './todo.service';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
import { AuthGuard } from '@nestjs/passport';

@UseGuards(AuthGuard())
@Controller('todo')
export class TodoController {
constructor(private readonly todoService: TodoService) {}

@Post()
create(@Body() createTodoDto: CreateTodoDto) {
return this.todoService.create(createTodoDto);
}

@Get()
findAll() {
return this.todoService.findAll();
}

@Get(':id')
findOne(@Param('id') id: string) {
return this.todoService.findOne(id);
}

@Patch(':id')
update(@Param('id') id: string, @Body() updateTodoDto: UpdateTodoDto) {
return this.todoService.update(id, updateTodoDto);
}

@Delete(':id')
remove(@Param('id') id: string) {
return this.todoService.remove(id);
}
}

And now we are done.

There is a a lot we can do enhance in this application, but I wanted it to be as simple as possible.

See the full code in my GitHub repo:

More content at PlainEnglish.io. Sign up for our free weekly newsletter. Follow us on Twitter, LinkedIn, YouTube, and Discord.

--

--

Robert Isaac
Robert Isaac

Written by Robert Isaac

I am a Senior Front-End Developer who fall in love with Angular and TypeScript.

Responses (5)