Luciano Serruya Aloisi

Develop and deploy Firebase-based REST APIs

Firebase is an awesome product suite. You can use its client SDKs to add back-end functionalities to your web or mobile apps, up to deploying full-stack applications to Firebase. In this short tutorial we will see how can we develop a Firebase-powered REST API and deploy it to Firebase, using Node.js and TypeScript.

Getting started

We will develop locally with Firebase, but we still need to use a Firebase project. Head over the Firebase console and create a new project. We will also need to have the firebase CLI tool installed in our machine, so install it running npm install -g firebase-tools.

Following with our scaffolding, we now need a NPM project. To create a new one, first create a new directory, cd into it, and start a new NPM project with npm init. We will build a REST API for storing contacts, so our project will be called contacts-on-fire

mkdir contacts-on-fire && cd contacts-on-fire && npm init -y

Once created, initialize your Firebase project locally in your directory via the command firebase init. Choose Functions and Emulators pressing the space bar, and choose your recently created project when prompted to select one. When setting up your functions, it will create a new functions directory (we will delete it and restructure the project) and install some dependencies. When setting up the emulators, select the Functions and Firestore emulators and use the default ports (or remember what ports you changed, you will have to update some files to make it work).

If you see what is inside the functions directory, you will see there is another package.json file with its corresponding node_modules directory, which means is a different NPM project. This brings some complications we don't want, so we will rescue the files we want, and delete the rest. Move to your root project the following files and directories

  • src directory
  • .eslintrc.js
  • tsconfig.json
  • .gitignore (in case you created a git repository)

You can now delete the functions directory

mv functions/src functions/.eslintrc.js functions/tsconfig.json functions/.gitignore . && rm -rf functions

As we are building a server-side application, we won't install any client-side Firebase SDK, but the admin one. We will also need to install the firebase-functions package.

npm install firebase-admin firebase-functions

We need to install some development-only dependencies as well, such as the TypeScript compiler, eslint, and some eslint plugins

npm install --save-dev typescript eslint eslint-plugin-import @typescript-eslint/eslint-plugin @typescript-eslint/parser

As we move our Cloud Functions code from the functions directory to our root project, we need to update the firebase.json and package.json files. Your firebase.json file should look like the following

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

The main update is the source entry, specifying where our code is located

Your package.json should look like this

...
"main": "lib/index.js",
"scripts": {
  "build": "tsc"
},
"engines": {
  "node": "10"
}
...

The main update is the main entry, specifying which file is the entry point for our Cloud Functions (Firebase looks for this file when deploying our Cloud Functions and when running the Emulators), the build script, and the engines entry

Open your src/index.ts file and uncomment the only Cloud Function definition in there

export const helloWorld = functions.https.onRequest((request, response) => {
  functions.logger.info("Hello logs!", {structuredData: true});
  response.send("Hello from Firebase!");
});

Run the Firebase emulators with firebase emulators:start and you shouldn't have any problems up to this point. Navigate to localhost:4000 (in case you used the default ports when setting up the Firebase Emulators), and you should see the Firebase Emulators UI. Click on the Functions section, look for your helloWorld function's URL, navigate to it using your browser, and it should return the message Hello from Firebase!. We are done with the Firebase set up, we can now start developing our REST API!

Coding time

It's time for us to start coding our application. Our contact model will only have three fields: firstName, lastName, and address. We will implement the following endpoints:

  • GET /api/contacts -> Retrieve all contacts
  • GET /api/contacts/:id -> Retrieve single existing contact
  • POST /api/contacts -> Create new contact
  • PUT /api/contacts/:id -> Update existing contact
  • DELETE /api/contacts/:id -> Delete existing contact

We can create a HTTP-based Cloud Function based of a express.js application. To do so, we first need to install express.js and some development-only dependencies

npm install express && npm install --save-dev @types/express

Then, in your src/index.ts file create a new express.js app and pass it to your Cloud Function

// src/index.ts

import * as functions from 'firebase-functions';
import express from "express"

const app = express()

app.get("/contacts", async (req, res) => {
  res.json({
     data: []
  })
});

export const api = functions.https.onRequest(app);

If you now try to run the Firebase Emulators or to compile your TS code with npm run build, you will see an error because of the way we are importing express. To fix it, add the following line to your tsconfig.json file

{
  "compilerOptions": {
    ...
    "esModuleInterop": true,
    ...
  }
}

Try again now, it should work.

Try changing anything from the code by yourself, you will realize that your changes aren't impacting your Cloud Function - that's because you have to stop the Emulators, and run them back again to recompile your code. To avoid having to stop them manually every time we make a change, let's install a really handy package to help us with this issue.

npm install --save-dev concurrently

We now have to update our npm-scripts to use concurrently

...
"scripts": {
  ...
  "dev": "npm run build && concurrently --kill-others-on-fail 'tsc -w' 'firebase emulators:start'",
  "prebuild": "rm -rf lib",
  "build": "tsc",
  ...
}
...

We added quite a few new npm-scripts. Now, to run the emulators, you would have to run npm run dev but only once, when you first start developing - you don't have to stop them every time you make a change to your code. This script will first build our code, then it will run two script in parallel (with the help of concurrently), and if one of them ends with a non-zero code (it ended because of an error), it will restart both the failing process and the other one

Storage

We have a basic endpoint that returns an empty array. We now can integrate our database (Firestore) into the mix. We can easily do so by initializing our app and retrieving a reference to our database

// src/index.ts
...
import {initializeApp, firestore} from "firebase-admin";

initializeApp();
const db = firestore()

Now, in your handler for your only route

app.get("/contacts", async (req, res) => {
  const { docs } = await db.collection("contacts").get();
  const contacts = docs.map(doc => ({ id: doc.id, ...doc.data() }));
  res.json({ data: contacts });
});

We are retrieving every document present in the "contacts" collection, and send it back to the client as an array of objects. Run the Emulators, go to the Firestore Emulator, create a new collection called "contacts" and add some documents. Hit your Cloud Function and you should see your recently created document

If you called your Cloud Function (the function your are exporting from src/index.ts) api, you can make a HTTP request to it via http://localhost:5001/<PROJECT_ID>/<REGION>/api. Use the correct port in case you changed it when setting up the Emulators. If not working, check the Functions Emulators log to see the URL

I'm not going to show how to code the rest of the endpoint, they are quite easy and repetitive. In case you'd like to see them, please refer to this repo.

Deployment

Because we already set up our Firebase project on the cloud and initialized locally, deploying our functions to our Firebase project is as simple as running firebase deploy

Conclusion

In this tutorial we saw how to locally develop a Firebase-powered REST API using express.js, Cloud Functions for Firebase, and Firestore, and deploy it to a serverless provider such as Firebase.

Hope you liked it!

🐦 @LucianoSerruya

📧 lucianoserruya (at) gmail (dot) com