Serverless - we've come to love it (or hate it). Personally, I like the idea of not having pulled my eyelashes out before the end of each week because of excruciating frustration with a self-managed stack. I'd also prefer not to add to my current level of anxiety and sleepless nights. So when I can use something like Firebase in a project, then I dive into the opportunity with complete enthusiasm.

Beautiful democracy

Firebase Functions, an extension of Google's Cloud Functions, allows you to write backend logic for your Firebase project. It provides features such as CRUD (Create Read Update Delete) triggers for Firestore and Realtime Database, and custom HTTP callable functions via a URL endpoint. You can even hookup the  .onPublish() trigger for PubSub, running your custom logic every time a new message is published to a specific topic in your Firebase's corresponding Google Cloud Platform project. This is my preferred way of using the recently introduced Cloud Scheduler to run time-based logic in my Firebase project. (Just kidding. I stole that preference from Jeff Delaney who said it was better as it is secure by default over here.)

Getting fired up with Firebase Functions

Okay, first thing's first. Put the kettle on. Alright, now quickly get back to your psychotic computer and caress the terminal with the following prose:

$ npm i -g firebase-tools

This (should) install firebase-tools globally and give you access to the all so helpful firebase command. If something went terribly wrong, then I may not be the guy to help you out. I've barely got my own s*** straight. Go to SO or something 🤷‍♂️.

Now you'll be ready to have some fun answering another BuzzFeed qui... I mean setting up the configuration for your Firebase Functions project.

$ mkdir firebase-functions && cd firebase-functions
$ firebase init

You will now be bombarded with more questions than your partner's mother asks during thanksgiving.

Look at the terminal extracts below. It should guide you through it. I hope.


######## #### ########  ######## ########     ###     ######  ########
##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
######    ##  ########  ######   ########  #########  ######  ######
##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:

  /Users/duncan/duncan.id/firebase-functions

? Which Firebase CLI features do you want to setup for this folder? Press Space to select features, then Enter to confirm your choices. 
 ◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
❯◉ Functions: Configure and deploy Cloud Functions
 ◯ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules

I chose not to set up a default project since I just wanted to show you the project structure we'll be setting up, okay? You can go ahead and  firebase login if you want to link your account and add a default project, or you can create a new project. I'll just be a rebellious teen and do neither of those things.

=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add,
but for now we'll just set up a default project.

? Select a default Firebase project for this directory:
❯ [don't setup a default project]
  [create a new project]

Now go ahead and select "no thanks, I want to remain relatively sane".

=== Functions Setup

A functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions?
  JavaScript
❯ TypeScript

You can just say yes to everything else. Unless it's for drugs. Then don't do it unless you have a guardian present that can do it with you with Bob Marley or My Little Pony blasting in the background.

When everything is done, you should be coming down from your imaginary high and the water you just boiled in the kettle should be lukewarm by now. Great job.

Now if we just stuffed every single function into index.ts (or index.js) and baked at 420℉, we should end up with something like this:

boy playing jenga
Don’t let your single entry spaghetti be the cause of your functions coming crashing down, eating you alive or giving you severe indigestion.

Nice! We summoned a giant spaghetti monster.

Just don't touch it.

Okay. So we can't deal with those kinds of problems right now, so let's just avoid it all together. We're going to split up our functions into a sensible structure.

Divide and contrive

For the purposes of this article, we'll put together a contrived example of taming the spaghetti monster in the hopes that the reader will adapt it to their own projects. Don't you feel lonely when I talk about you in third person?

Your project structure should looks something like this now:

We can add some aptly named functions to index.ts to check when our pasta and meatballs are created or deleted on Firestore via Firestore triggers.

import * as functions from 'firebase-functions';

export const makePasta = functions.firestore.document('pastas/{pastaId}')
    .onCreate((snapshot, context) => {
        const note = `
            The pasta with id, ${context.params.pastaId} was made
            at ${snapshot.createTime.seconds} seconds past the epoch.
        `;
        console.log(note);
});

export const eatPasta = functions.firestore.document('pastas/{pastaId}')
    .onDelete((snapshot, context) => {
        const note = `
            The pasta with id, ${context.params.pastaId} was eaten.
        `;
        console.log(note);
});

export const makeMeatball = functions.firestore.document('meatballs/{meatballId}')
    .onCreate((snapshot, context) => {
        const note = `
            The meatball with id, ${context.params.meatballId} was made
            at ${snapshot.createTime.seconds} seconds past the epoch.
        `;
        console.log(note);
});

export const eatMeatball = functions.firestore.document('meatballs/{meatballId}')
    .onDelete((snapshot, context) => {
        const note = `
            The meatball with id, ${context.params.meatballId} was eaten.
        `;
        console.log(note);
});

We can pull these functions out of index.ts and place them in more relevant directories in src.

In src/meatballs/index.ts you can put the functions relevant to meatballs:

import * as functions from 'firebase-functions';

export const makeMeatball = functions.firestore.document('meatballs/{meatballId}')
    .onCreate((snapshot, context) => {
        const note = `
            The meatball with id, ${context.params.meatballId} was made
            at ${snapshot.createTime.seconds} seconds past the epoch.
        `;
        console.log(note);
});

export const eatMeatball = functions.firestore.document('meatballs/{meatballId}')
    .onDelete((snapshot, context) => {
        const note = `
            The meatball with id, ${context.params.meatballId} was eaten.
        `;
        console.log(note);
});

And in src/pastas/index.ts you can put the functions relevant to pastas:

import * as functions from 'firebase-functions';

export const makePasta = functions.firestore.document('pastas/{pastaId}')
    .onCreate((snapshot, context) => {
        const note = `
            The pasta with id, ${context.params.pastaId} was made
            at ${snapshot.createTime.seconds} seconds past the epoch.
        `;
        console.log(note);
});

export const eatPasta = functions.firestore.document('pastas/{pastaId}')
    .onDelete((snapshot, context) => {
        const note = `
            The pasta with id, ${context.params.pastaId} was eaten.
        `;
        console.log(note);
});

Finally, back in the original src/index.ts, you can delete the previous functions and the firebase-functions import. Then you can import and export the meatballs and pastas functions:

import * as Meatballs from './meatballs/index';
import * as Pastas from './pastas/index';

// Export the meatball functions:
export const makeMeatball = Meatballs.makeMeatball;
export const eatMeatball = Meatballs.eatMeatball;

// Export the pasta functions:
export const makePasta = Pastas.makePasta;
export const eatPasta = Pastas.eatPasta;

Okay, I have a kettle to reboil. Ciao!