Logo
Published on

How to Split Firebase Cloud Functions for Better Code Organization

Introduction

Firebase Cloud Functions are a great way to extend the functionality of your Firebase project. They allow you to run backend code in response to events triggered by Firebase features and HTTPS requests.

Table of Contents

Often times, you may hear that for Node.js development, the code logic should be separated into different modules.

This is logical and obviously the best practice whenever you are writing code for the real world.

In Firebase Cloud Functions, you can also do that.

From your index.js file, simply require() the function from another file and use its reference to call it.

Move Code From index.js

For this example, I'm directly going to the index.js file.

Instead of going through a walkthrough of the firebase init and other miscellaneous things you have to do, let's see the index.js file where multiple functions are defined in the same file.

This is your typical approach as a beginner where ALL your stuff is inside a single file.

// index.js

const functions = require('firebase-functions')

/**
 * Returns the server timestamp on request.
 *
 * @param {Object} req Express Request Object
 * @param {Object} res Express Request Object
 * @returns {string} A date object.
 */
const getTimestamp = (req, res) => {
  res.end(new Date().toString())
}

/**
 * Returns a string 'Hi' + argument on request.
 *
 * @param {Object} req Express Request Object
 * @param {Object} res Express Request Object
 */
const sayHi = (req, res) => {
  const name = req.query.name || 'there'
  res.end(`Hi ${name}!`)
}

module.exports = {
  'get-timestamp': functions.https.onRequest(getTimestamp),
  'say-hi': functions.https.onRequest(sayHi),
}

In this project, we have a single .js file where the code lives. The folder structure is as follows:

│ index.js
│ package-lock.json
│ package.json
└───node_modules

Split Simple Functions Into Multiple Files

As you can see, the two functions getTimestamp and sayHi don't do much.

The getTimestamp function returns the server timestamp on request to the /get-timestamp endpoint.

The sayHi function returns the string 'Hi' + name or Hi there depending on the query parameter in the request.

You can split them into separate files for each. Make separate files get-timestamp.js and say-hi.js in the same (or inside a /src) folder with the code.

Remove the getTimestamp and sayHi functions from the index.js.

// get-timestamp.js

/**
 * Returns the server timestamp on request.
 *
 * @param {Object} req Express Request Object
 * @param {Object} res Express Request Object
 * @returns {string} A date object.
 */
const getTimestamp = (req, res) => {
  res.end(new Date().toString())
}

module.exports = getTimestamp
// sayHi.js

/**
 * Returns a string 'Hi' + argument on request.
 *
 * @param {Object} req Express Request Object
 * @param {Object} res Express Request Object
 */
const sayHi = (req, res) => {
  const name = req.query.name || 'there'
  res.end(`Hi ${name}!`)
}
module.exports = sayHi
// index.js

const functions = require('firebase-functions')

const sayHi = require('./say-hi.js')
const getTimestamp = require('./get-timestamp.js')

module.exports = {
  'get-timestamp': functions.https.onRequest(getTimestamp),
  'say-hi': functions.https.onRequest(sayHi),
}

The file structure looks like this after splitting our code:

│ index.js
│ get-timestamp.js
│ say-hi.js
│ package-lock.json
│ package.json
└───node_modules

All this worked very nicely because the Firebase Cloud Functions use the Node.js environment to run your Javascript code.

But, what if, we want to do more? Like reading from Firestore, or handling users in Firebase Auth?

Splitting Cloud Functions With Admin SDK

Let's see what the problem is.

Initialize the project with the same index.js file and add the Admin SDK to it.

This function reads a document from Firestore and sends it as a JSON response onRequest().

// index.js

const functions = require('firebase-functions')
const admin = require('firebase-admin')

admin.initializeApp()

const db = admin.firestore()

/**
 * Reads a doc from Firestore and sends it's data in the response.
 *
 * @param {Object} req Express Request Object
 * @param {Object} res Express Request Object
 */
const getDoc = (req, res) => {
  db.doc('doc-id')
    .get()
    .then((doc) => {
      res.json(doc.data())
    })
    .catch((error) => console.log(error))
}

module.exports = {
  'get-doc': functions.https.onRequest(getDoc),
}

On curl-ing a request to the /get-doc endpoint, you will get the data of the document with the id: doc-id.

Here, we are using the Admin SDK's functionality where it allows us to read documents from Firestore.

Let us now try to move the getDoc function to its own separate file.

// index.js

const functions = require('firebase-functions')
const getDoc = require('./get-doc')

module.exports = {
  'get-doc': functions.https.onRequest(getDoc),
}
// get-doc.js

const admin = require('firebase-admin')
const serviceAccount = require('./key.json')

admin.initializeApp()

const db = admin.firestore()

/**
 * Reads a doc from Firestore and sends it's data in the response.
 *
 * @param {Object} req Express Request Object
 * @param {Object} res Express Request Object
 */
const getDoc = async (req, res) => {
  return db
    .doc('doc-name')
    .get()
    .then((doc) => {
      return res.json(doc.data())
    })
    .catch((error) => console.log(error))
}

module.exports = getDoc

Since the get-doc requires Admin functionality, we also need to initialize the Admin SDK in that file.

And, herein lies the problem.

In Firebase Cloud Function, you cannot initializeApp() more than once.

What if you want to read a doc from Firestore which is in another file.

You might try to use the initializeApp() method and require-ing the admin-sdk in that file too.

But, that will throw an error.

Error: The default Firebase app already exists. This means you called initializeApp() more than once without providing an app name as the second argument. In most cases, you only need to call initializeApp() once. But if you do want to initialize multiple apps, pass a second argument to initializeApp() to give each app a unique name.

The fix to this problem is to create a separate file where you initialize the Admin SDK and require it everywhere you want to use it.

So, the final structure of your app will look like this:

│ admin.js
│ index.js
│ get-doc.js
│ package-lock.json
│ package.json
└───node_modules
// admin.js

const functions = require('firebase-functions')
const admin = require('firebase-admin')

admin.initializeApp()
const db = admin.firestore()

module.exports = {
  db,
}
// get-doc.js

const { db } = require('./admin')

/**
 * Reads a doc from Firestore and sends it's data in the response.
 *
 * @param {Object} req Express Request Object
 * @param {Object} res Express Request Object
 */
const getDoc = async (req, res) => {
  return db
    .doc('doc-name')
    .get()
    .then((doc) => {
      res.json(doc.data())
    })
    .catch((error) => console.log(error))
}

module.exports = getDoc
// index.js

const functions = require('firebase-functions')
const getDoc = require('./get-doc')

module.exports = {
  'get-doc': functions.https.onRequest(getDoc),
}

Now, you can use the require() function to import the db object from the Firebase Admin SDK to use it anywhere in your project.