- 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.