How to structure a serverless REST API with Firebase Functions + Express

Creating a simple script with Functions is easy. Once you go beyond trivial, intelligent file structure will really matter.

Jason Byrne
Nov 17, 2019 · 6 min read

Firebase Functions is a simple yet powerful tool for your project. It can be used much the same way Lambdas are in the AWS world. They act as microservices that respond to various events. That can include a database value changing in Firebase RTDB or Firestore or a file being written to Storage. Additionally it can be wired to respond directly to HTTP requests, allowing you to use it to build a RESTful API (via Firebase Hosting as the gateway).

So the first thing to do is create your base project with the Firebase CLI. There is no need for me to go over that because Google’s documentation does a perfectly great job:

For my case, I am going to be using Firestore, Functions, and Hosting. This will allow me to have both database triggers and a RESTful API. My base file structure now looks like this:

I want to set up a web page under Hosting that can serve as the front end of my site, but I also want to have an API that will map to Functions. So anything in my Hosting domain that starts with /api/ I will want to map accordingly.

So I add this into my firebase.json

"hosting": {
"public": "public",
"rewrites": [
{
"source": "/api/**",
"function": "api"
}
],

"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}

I could get more granular than this and route certain API calls to different Functions, but for now I’m just going to map it all into the same one… which is a function called “api” that I will use Express on to route further.

Within my Firebase Functions, I am going to use TypeScript because I am a modern and disciplined developer. Because only a sadomasochist would want to use plain old un-typed JavaScript.

So, within our functions folder, we will create subfolders called src and dist and then we’ll configure TypeScript with a tsconfig.json like this…

{
"compilerOptions": {
"module": "commonjs",
"noImplicitReturns": true,
"noUnusedLocals": true,
"outDir": "dist",
"sourceMap": true,
"strict": true,
"target": "es2017"
},
"compileOnSave": true,
"include": [
"src"
]
}

Initialize your project with npm init and then set up your dependencies. Your specific dependency and dev dependency needs may vary, but this is what my package.json looks like:

To start out with, let’s go ahead and create four files in our src folder:

  • index.ts

What we want to accomplish is that each of those files will have just one purpose. The firebase.ts will be for configuring and exporting your Firebase application and references to any of the services you will use.

You want this to be the ONLY file where this is done. You do not want to include or initialize your application in multiple places. In my simple example, I will only be using Firestore so that is the only thing I need to export.

Next we need to configure Express, which is the HTTP framework that will handle our requests. We will use rest.ts this explicit purpose, including configuring the different middleware helpers we’ll need to process common REST API requests.

We did a lot here, so as a quick break down here’s what the use parts above actually did for us.

  • The requests will come in as /api/something (routed from our Hosting rewrite toward the top of this article). I don’t want to have to type the “/api” part of it over and over. So I just strip that from the request.url value, feel free to leave this out if you want but then you’ll need that api part in all of your routes later.

At the very end, we return our Express application and we’ll use this later in our index.ts. However, first let’s call out the routes method. This will be a call out to our next file whose sole job is setting up the different routes for the API methods we’ll support.

So here is our routes.ts file:

At this point, it is high time I point out we are using dependency injection. You will notice I’m passing around the reference to db so that we only have to initialize it once. I’m not actually going to use this Firestore connection this tutorial, but if we were actually going to create a full-blown API to do something meaningful then we’d need it. So that’s why it’s in here.

Finally, lets add our index.ts file that will tie it all together.

In this file, we are creating a Firebase Function called api (because that’s what we’re exporting it as), which will pass all of the https requests to our Express application that we previously configured. Notice we also can define the settings of the serverless instance that it will execute with.

Later down the line, we would add additional Firebase Functions into this same index.ts file, such as Firestore data triggers perhaps.

Now what’s next? I am going to avoid too deep for this article, but I would create additional folders such as:

  • endpoints

In my routes.ts file, I am not going to actually do anything other than hand the processing off to other files. Remember, each file should do just one thing or a very limited subset of things.

So I would recommend creating a file for each support end point. I will put them in the endpoints folder and name the file like {httpMethod}_{path}.ts so for example: endpoints/get_articles.ts.

So then in your routes file you will import it:

import { get_articles } from ‘./endpoints/get_articles’;

And then call that handle from that route, passing in any references that it will need to process it:

app.get('/articles', (req: Request, res: Response) => {
get_articles(req, res, db);
return;
});

One more tip that I’ll offer if you want to take your clean API code to the next level. Master writing Express middleware. This is such a handy tool to make code that is reusable, concise, and highly readable.

For example, you will probably find yourself writing authentication logic in your API. And then you’ll start copying and pasting similar logic in all these different methods that need to enforce it.

NO! STOP DOING THAT! Just write one file that handles it and then you can put it right in with part of our route. Check it out.

Here is an example file that I’ll locate in helpers/auth.ts to check that there was a bearer token added to the request. For simplicity this example, it won’t actually validate the content. It just checks that there is a token of some sort.

If the authentication is deemed to success, we’ll call the next() callback that will signal Express to continue to the next step in processing the request. However, if there was no token then we bailed out early and said 401.

So now back in our routes.ts file we can add this middleware step to whatever endpoints require authentication.

First import this auth helper file:

import { requireAuth } from ‘./helpers/auth’;

And then in our same articles route from above, we just add the authRequired argument and that is it!!

app.get('/articles', requireAuth, (req: Request, res: Response) => {
get_articles(req, res, db);
return;
});

If you have additional requirements, like not just requiring an authenticated user but perhaps admin permissions, just add another middleware that checks for that entitlement. Then add another argument to the chain. It is so easy and so clean!

I hope this helps get you kick started to write some highly readable, easily maintainable, separation of concerns loving, totally DRY, serverless APIs based on Firebase Functions!

Jason Byrne

Written by

Entrepreneur, developer, historian, journalist, Christian, family man, and track & field fan. VP of Software Development @ FloSports. Founder of MileSplit.

More From Medium

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade