Firebase is one of the leading solutions for building serverless apps. It offers developers a way to easily develop scalable, high-quality apps without worrying about the cost of setting up and maintaining servers over the long term from the onset. Firebase also offers easy integration with Google services like Google Analytics and the Firebase document database, Firestore.
In this article, we’ll see how to build a REST API with Firebase cloud functions, TypeScript, and Firestore. You’ll also need a bit of knowledge about Express.js to build our demo app. However, you don’t need to be a pro in any of the aforementioned technologies to follow along. Our demo app will be a journal API that’ll be able to accept new entries, and retrieve, update, and delete existing entries.
Setting up Firebase
To get started with our app, you’ll need to have Node.js installed on your computer. Click here to see the installation guide for Node.js. If you already have it installed, running the following command on your terminal should return your current Node.js version:
node --version
To follow this tutorial without any warnings, you’ll need to install the same version of Node.js we’ll use in the Firebase cloud. Currently, Firebase SDK supports Node.js 12 and 10. Let’s use version 12 for our app. The Node.js installation comes with npm, which we’ll use to install the Firebase CLI. Let’s run the following command on our terminal to do that:
npm install -g firebase-tools
With Node.js and the Firebase CLI installed, you can go ahead and navigate to the Firebase console on your browser. You’ll be required to sign in to your Google account. After a successful sign-in, you should see a Create a project button that looks like this:
Depending on when you follow this walkthrough, the user interface may look a bit different, but you should still be able to find your way around. When you click on the Create a project button, you’ll find the process of creating a Firebase project straightforward.
Next, you’ll be given the option of choosing a project name and ID. I’ll use journal-rest-api for mine. Since the ID is a unique identifier for your project, you’ll need to pick a different project ID. With that done, you can successfully create your Firebase project.
After a successful creation, you should see a project overview page that looks like this:
Upgrading our billing plan
By default, your new account should be on the Spark Free Billing plan. However, to be able to use Firebase functions, you’ll need to upgrade your billing plan to the Blaze Pay as you go. We don’t have to worry about money for this demo, but Firebase nevertheless requires that you upgrade your billing plan to be able to use the cloud functions.
To upgrade your billing plan, open the Develop dropdown in the left-hand nav and click on Functions. On the Functions page, you’ll be prompted to upgrade your billing. The message should look like this:
Creating a Firestore database
With our billing plan upgraded, let’s create a Firestore database for our app. Navigate to the Cloud Firestore option on the left pane, then click on the Create database button. You should see a modal that looks like this:
We’ll use the Start in production mode option for this demo. With our database created, we can now write and deploy our first cloud function.
Writing our first cloud function
To write our fist cloud function, we’ll go back to our terminal and run the following command to sign in to our firebase account:
firebase login
Running the command should take you to an authentication page on your browser. After a successful authentication, create a project directory on your computer and, inside it, run the following command to initialize Firebase functions:
firebase init functions
You can go with the default options for your setup. Select the Use an existing project option, then chose the project you just created on Firebase. When asked What language would you like to use to write Cloud Functions? Select TypeScript. Finally, accept the option to install the npm dependencies. If everything goes well, you should see a success message that looks like this:
The project structure for our new app should look like this:
├── .firebaserc ├── .gitignore ├── .firebase.json └── functions ├── package.json ├── tsconfig.json ├── .gitignore └── src ├── index.ts
Notice that the package.json
file is inside the functions
directory. This means that when running our npm
commands, we would need to navigate to the functions directory on our terminal. When you open the ./functions/src/index.ts
file, you should see this:
import * as functions from 'firebase-functions'; // // Start writing Firebase Functions // // https://firebase.google.com/docs/functions/typescript // // export const helloWorld = functions.https.onRequest((request, response) => { // functions.logger.info("Hello logs!", {structuredData: true}); // response.send("Hello from Firebase!"); // });
Uncommenting the helloworld
function should give us this:
import * as functions from 'firebase-functions'; export const helloWorld = functions.https.onRequest((request, response) => { functions.logger.info("Hello logs!", {structuredData: true}); response.send("Hello from Firebase!"); });
Deploying our first function
To deploy the default helloworld
function, navigate to the functions directory on your terminal and run the following command:
npm run deploy
If the deployment is successful, you should receive a response that looks like this:
With that done, when we navigate to the Functions section of our Firebase console, we should see our deployed helloWorld
function and its URL. Navigating to the displayed URL on our browser should return the response Hello from Firebase!
If our goal is to make simple API calls and database queries, what we’ve done so far should be enough. However, since we want to build a more robust REST API with middleware and multiple routes, we’ll need a Node.js framework like Express. To install Express.js, let’s run the following code on our terminal — remember, we’re still in the functions directory:
npm i -s express
With Express installed, we can initialize it in our app by replacing the code in the ./index.ts
file with the following:
import * as functions from 'firebase-functions' import * as express from 'express' const app = express() app.get('/', (req, res) => res.status(200).send('Hey there!')) exports.app = functions.https.onRequest(app)
Next, let’s redeploy our app by running npm run deploy
on our terminal. When prompted with a warning about the deletion of our initial helloWorld
function, select yes.
The following functions are found in your project but do not exist in your local source code: helloWorld(us-central1) ... Would you like to proceed with deletion? Selecting no will continue the rest of the deployments. (y/N)
With that done, when you revisit the functions page on your Firebase console, you should see a new function named app and its request URL beside it. Navigating to that URL should return the response “Hey there!” as instructed on line 5
of our ./index.ts
file:
app.get('/', (req, res) => res.status(200).send('Hey there!'))
Creating a service account for our app
In order to access our Firestore database and admin tools from our app, we’ll need to create a service account. To do this, click on the gear icon next to Project Overview in the left pane:
From the Settings page, navigate to Service accounts, select Node.js, and click on the Generate new private key button.
This should generate a JSON configuration file for your new service account. Here’s what the JSON file should look like:
{ "type": "service_account", "project_id": "journal-rest-api", "private_key_id": "private_key_id", "private_key": "private_key", "client_email": "client_email", "client_id": "client_id", "auth_uri": "auth_url", "token_uri": "token_url", "auth_provider_x509_cert_url": "auth_provider_x509_cert_url", "client_x509_cert_url": "client_x509_cert_url" }
Accessing our service account config
An easy way to use our new service account information would be to move the JSON file to our project directory and then import it where it’s needed. However, it’s recommended to keep this information private, and if our codebase is pushed to a remote repository, storing it in the project directory will expose it to public view.
A better way to use the information from our service account is by storing the needed key-value pairs as our Firebase environment variables. For our application, we’ll need the private_key
, project_id
, and client_email
. Let’s store these as environment variables. To do so, we’ll go back to our terminal and run the following command:
firebase functions:config:set private.key="YOUR API KEY" project.id="YOUR CLIENT ID" client.email="YOUR CLIENT EMAIL"
Before running the above command, remember to substitute the string values with the one in your JSON file. It’s important to note that only lowercase characters are accepted in keys, and each of the keys should be namespaced using periods — for example, private.key
. If the update is successful, you should receive a response that looks like this:
+ Functions config updated. Please deploy your functions for the change to take effect by running firebase deploy --only functions
Let’s re-deploy our functions for the changes we’ve made to take effect:
npm run deploy
Setting up our Firebase config
Now that we have our service account information as Firebase environment variables, let’s set up our config file. We’ll create a new folder named config
in the ./functions/src
directory, and inside it, a new file named firebase.ts
. Here’s what the structure should look like:
└── functions └── src └── config ├── firebase.ts
Let’s paste the following code inside the firebase.ts
file:
import * as admin from 'firebase-admin' import * as functions from 'firebase-functions' admin.initializeApp({ credential: admin.credential.cert({ privateKey: functions.config().private.key.replace(/\\n/g, '\n'), projectId: functions.config().project.id, clientEmail: functions.config().client.email }), databaseURL: 'https://[your_project_id].firebaseio.com' }) const db = admin.firestore() export { admin, db }
We started by importing the Firebase admin for initializing our app and Firebase functions for accessing the Firebase environment variables. We went on to call the admin.initializeApp()
function and supplied an object argument to it, which contains our service account credentials. We’ve also used functions.config().name_of_variable
to access our environment variables.
Notice that we added a .replace()
method to the private key. This replaces \\n
in the private key string to \n
. Without this, we would get an invalid PEM format
error message when we try to use the variable. We also added a databaseUrl
key after the credential
.
You should replace [your_project_id]
with the one you provided when creating your Firebase project. Next, we created a variable for our Firestore database named db
and gave it the value admin.firestore()
. Finally, we exported admin
and db
from the firebase.ts
file.
Working with Firestore
Firestore is a NoSQL database for mobile, web, and server development. It offers a seamless integration with Firebase cloud functions, and its flexible and scalable nature makes it a good fit for building web APIs. Like all NoSQL databases, Firestore is non-relational and is made up of documents and collections. These documents contain a set of key-value pairs and are stored in collections.
In Firestore, documents and collections can be created from the Firebase console or through cloud functions. To create documents or collections from our cloud functions, we’ll use the db
we exported from the Firebase config file:
db.collection('entries').doc().create({ document_object_here })
The line of code above will create a new document in the collection named entries
, and if such a collection does not exist, a new one will be created with the provided name. We can also create an empty document and then provide key-value pairs for it later on:
const entry = db.collection('entries').doc() const entryObject = { id: entry.id, title: 'entry title here', text: 'entry text here' } entry.set(entryObject)
Firebase documents have a .set()
method that can be used to update them. We’ll see how to use a few other methods as we progress.
Adding entries to our Firestore database
With our database config set up, we can go ahead and add functionality for creating journal entries. Inside the ./functions/src
directory, let’s create a new file named entryController.ts
. We’ll use this file to house all the controllers for our entry. Inside the entryController.ts
file, we’ll start by importing our Response
type from express
and db
from ./config/firebase
:
import { Response } from 'express' import { db } from './config/firebase'
Next, we’ll define a type for our entries and then use it to create our Request
type:
... type EntryType = { title: string, text: string, coverImageUrl: string } type Request = { body: EntryType, params: { entryId: string } }
For the Request
type, we included entryId
as a property of params
. We’ll use this property when we want to edit an entry. Next, let’s create our addEntry
function:
... const addEntry = async (req: Request, res: Response) => { const { title, text } = req.body try { const entry = db.collection('entries').doc() const entryObject = { id: entry.id, title, text, } entry.set(entryObject) res.status(200).send({ status: 'success', message: 'entry added successfully', data: entryObject }) } catch(error) { res.status(500).json(error.message) } }
In our addEntry
function, we started by destructuring title
and text
from the request body. Next, we created a new Firebase document using the db.collection().doc()
method and then updated the document with our entryObject
. Our new document comes with an id
. We’ll need to store this id
as part of its key-value pairs so that we can easily access it.
res.status(200).send()
returns a response to our user if the API request is successful. We also wrapped our database actions in a try…catch
statement so that if there’s an error with our database requests, the catch block will respond with a 500
error and message from the error
object.
The 500
HTTP status code does not cover user input errors or other types of errors, so we’ll need to cover those cases before making our database requests or use a middleware for that purpose.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to animate your React app with AnimXYZ
- Explore Tauri, a new framework for building binaries
- Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
To use our addEntry
function, let’s export it from the entryController.ts
file:
... export { addEntry }
Next, we’ll import it in our ./functions/src/index.ts
file:
... import { addEntry } from './entryController'
Then, we’ll add the following code below the line where we initialized express
:
app.post('/entries', addEntry)
Our index.ts
file should now look like this:
import * as functions from 'firebase-functions' import * as express from 'express' import { addEntry } from './entryController' const app = express() app.get('/', (req, res) => res.status(200).send('Hey there!')) app.post('/entries', addEntry) exports.app = functions.https.onRequest(app)
To redeploy our app, we’ll go back to the Functions directory on our terminal and run the following command:
npm run deploy
If our deployment is successful, we should get a response that looks like this:
+ functions: Finished running predeploy script. i ... + functions[app(us-central1)]: Successful update operation. + Deploy complete!
To test our API, let’s navigate to the Functions section of our Firebase console. We should see our deployed function name app
and its URL. To add a new entry to our Firestore database, we’ll append the route /entries
to our URL. It should look like this: https://us-central1-project-id-here.cloudfunctions.net/app/entries
.
Then, we’ll send a POST
request using an API client like Postman or Insomnia to our URL with the properties title
and text
in our request body:
{ "title": "My first entry", "text": "Hey there! I'm awesome!" }
We should get a similar response to this:
{ "status": "success", "message": "entry added successfully", "data": { "id": "g8fBsSVQWlkby9GMf0LC", "title": "My first entry", "text": "Hey there! I'm awesome!" } }
Getting entries from our Firestore database
Now that we’ve seen how to add entries to our Firestore database using cloud functions, let’s see how we can get data from it. For this, we’ll create a new function named getAllEntries
in the entryController.ts
file:
... const getAllEntries = async (req: Request, res: Response) => { try { const allEntries = await db.collection('entries').get() return res.status(200).json(allEntries.docs) } catch(error) { return res.status(500).json(error.message) } } export { addEntry, getAllEntries }
In our getAllEntries
function, we’ve used the db.collection('entries').get()
method to retrieve all the data in our entries
collection and then accessed the documents in our response statement using the docs
property. If we try using this function to retrieve entries through an API client, we’ll get a response that includes sensitive information like our private key. Let’s avoid this by updating our function to return only the entry data:
const getAllEntries = async (req: Request, res: Response) => { try { const allEntries: EntryType[] = [] const querySnapshot = await db.collection('entries').get() querySnapshot.forEach((doc: any) => allEntries.push(doc.data())) return res.status(200).json(allEntries) } catch(error) { return res.status(500).json(error.message) } }
In our getAllEntries
function, we started by creating an array named allEntries
with type EntryType[]
. Next, we assigned the data from our db.collection().get()
method to a variable named querySnapshot
. In Firestore, a QuerySnapshot
contains the result of a query. The forEach
method loops through our QuerySnapshot
and pushes the document data to our allEntries
array.
To use our getAllEntries
function, we’ll import it in the ./functions/src/index.ts
file and then add it to a GET
request method:
... import { addEntry, getAllEntries } from './entryController' const app = express() app.get('/', (req, res) => res.status(200).send('Hey there!')) app.post('/entries', addEntry) app.get('/entries', getAllEntries)
Let’s re-deploy our app and make a GET
request to ./entries
. We should receive a response that looks like this:
[ { "text": "Hey there! I'm awesome!", "id": "nS4so0TiUI0YLQH3xZLT", "title": "My first entry" } ]
Updating and deleting entries
Now that we’ve seen how to make POST
and GET
requests from Firebase cloud functions to our Firestore database, let’s see how we can update and delete entries. To add the functionality for updating entries, we’ll create a new function named updateEntry
in the entryController.ts
file:
... const updateEntry = async (req: Request, res: Response) => { const { body: { text, title }, params: { entryId } } = req try { const entry = db.collection('entries').doc(entryId) const currentData = (await entry.get()).data() || {} const entryObject = { title: title || currentData.title, text: text || currentData.text, } await entry.set(entryObject).catch(error => { return res.status(400).json({ status: 'error', message: error.message }) }) return res.status(200).json({ status: 'success', message: 'entry updated successfully', data: entryObject }) } catch(error) { return res.status(500).json(error.message) } }
In our updateEntry
function, we started by fetching the requested document by its ID provided in the request params. We used the db.collection().doc()
method for this. Next, we got the current document data object with the .data()
method so that when we update an entry with the .set()
method, we can replace any field not provided by the user with the current entry data.
Let’s also create a function for deleting entries:
... const deleteEntry = async (req: Request, res: Response) => { const { entryId } = req.params try { const entry = db.collection('entries').doc(entryId) await entry.delete().catch(error => { return res.status(400).json({ status: 'error', message: error.message }) }) return res.status(200).json({ status: 'success', message: 'entry deleted successfully', }) } catch(error) { return res.status(500).json(error.message) } }
Our deleteEntry
function also gets the document using the entryId
from our request params and the db.collection().doc()
method. Next, we used the delete
method to delete our entry. We also added the catch
method to watch for errors and return a response if our database operation is unsuccessful.
To use our new functions, we’ll export them from the entryController.ts
file:
... export { addEntry, getAllEntries, updateEntry, deleteEntry }
We can now import them in the ./functions/src/index.ts
file:
... import { addEntry, getAllEntries, updateEntry, deleteEntry } from './entryController'
With the functions imported, let’s add the patch
and delete
routes:
... app.patch('/entries/:entryId', updateEntry) app.delete('/entries/:entryId', deleteEntry)
Our index.ts
file should now look like this:
import * as functions from 'firebase-functions' import * as express from 'express' import { addEntry, getAllEntries, updateEntry, deleteEntry } from './entryController' const app = express() app.get('/', (req, res) => res.status(200).send('Hey there!')) app.post('/entries', addEntry) app.get('/entries', getAllEntries) app.patch('/entries/:entryId', updateEntry) app.delete('/entries/:entryId', deleteEntry) exports.app = functions.https.onRequest(app)
To test the new functions, we’ll re-deploy our app using the npm run deploy
command and then send a PATCH
or DELETE
request to /entries/:entryId
using an API client like Postman. Remember that we included an entryId
in the body of our entries when creating them. We can get those IDs by sending a GET
request to ./entries
.
Conclusion
In this article, we’ve seen how to set up Firebase and use cloud functions to build a REST API with TypeScript and Firestore. We did not cover all the Firestore methods in this demo, but you can find them in the official docs, and with your knowledge from this article, they’ll be easier to understand.
In a production-ready app, you’ll need to set up middleware to handle a lot of cases; for example, we could have a middleware that checks whether an entry exists before trying to update it. We could also have an authentication and authorization middleware (here’s a good way to handle authentication with Firebase React apps) just before the controller functions. With our app structure, adding middleware will be straightforward.
Here’s a link to the GitHub repo for our demo app. Feel free to reach out to me on LinkedIn if you need any help!
LogRocket: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Try it for free.
Hello Ebenezer, i like your article, very clear and concise.
I have a little more interest in the section “Getting entries from our Firestore database”
In fact, i m pretty a newbe with Firestore, and i have an app developped with Flutter which store its data in Firestore.
So now i would like to retrieve this data and send it to another app (precisely Salesforce) , but don t know really how to proceed… i guess , i m suppose to write the class also directly in firestore or ?
Thank you in advance
What does the `-s` do in the following command, `npm i -s express`?
The `-s` flag is short for –save. It adds the package to the dependencies in your package.json
Forgive me, but it looks from the docs like there is no `-save` flag (anymore?). And: “npm install saves any specified packages into dependencies by default.”
Thanks for this doc, it really helps with initial setup. How would you go about setting this up to test with the local emulator? Any chance you could add a section or comment that just extends your current implementation to also use the local emulators correctly for local dev?