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 contactsGET /api/contacts/:id
-> Retrieve single existing contactPOST /api/contacts
-> Create new contactPUT /api/contacts/:id
-> Update existing contactDELETE /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