ClojureScript on Firebase Cloud Functions

If AWS was built from scratch for a simple developer experience, you might end up with something like Firebase. Originally launched in 2011 as a real-time database, Firebase was ingested by Google in 2014, in what could be described as an ease-of-use acquisition. Over the past five years, Google has grown Firebase into an ever-expanding layer of comfort and simplicity built on top of Google Cloud.

In roughly the same timeframe, ClojureScript has matured into a robust and easy-to-use Clojure experience for JavaScript. Thanks to continual improvements to the compiler and the appearance of shadow-cljs, setting up a ClojureScript project that targets the browser or node.js has become a relatively simple affair. Many of the old pains have been reduced or eliminated.

The autumn of 2019, therefore, is a great time for a tasty pairing of these two snacks. In this article, we're going to learn how to set up a small Node.js service in ClojureScript, running as a Firebase Cloud Function, and delivered with Firebase Hosting.

Our architecture will be as follows:

What we like about this setup:

For a real-world example of an app built on this stack, have a look at our recent experiment, Gist Press.

Prerequisites

You'll need to be familiar with working in a terminal, and have node.js version 8.13 or greater installed on your system (I recommend NVM for managing node.js versions).

Let's begin!

Create a project directory and install dependencies

From your terminal, create a new directory for this project, and a package.json file with the following content:

 {"engines":  { "node": "8" }}

This is required to inform Firebase which runtime to use for our function.

Now install shadow-cljs and firebase-tools as dev dependencies. We're using yarn here, but feel free to use npm.

yarn add --dev shadow-cljs firebase-tools

Install firebase-functions and firebase-admin as ordinary dependencies:

yarn add firebase-functions firebase-admin

Configuration files

To get started, we'll need to set up a couple of configuration files.

1. Create a shadow-cljs.edn file with the following content:

{:source-paths ["src"]
 :builds
 {:functions
  {:target      :node-library
   :output-to   "functions/index.js"
   :exports-var cljs-firebase.core/cloud-functions}}}

The :node-library target will, as its name suggests, produce code optimized for node.js. We must :output-to the path functions/index.js, because this is where Firebase expects to find our function index. The :exports-var option specifies that index.js will export whatever we define at cljs-firebase.core/cloud-functions.

2. Create a firebase.json file:

{
  "hosting": {
    "rewrites": [
      {
        "source": "**",
        "function": "handleRequest"
      }
    ],
    "public": "public",
    "predeploy": "yarn shadow-cljs release functions && cp package.json functions/package.json && mkdir -p public"
  }
}

Our hosting config includes a rewrite rule that will redirect all requests (**) to our handleRequest function. We also specify a public directory that will be uploaded on deploy. (Requests that match a file in the public directory will return that file, and not be forwarded to our function.) Lastly, our predeploy command will be run automatically before each deploy. In it, we compile our functions, copy our package.json into the functions directory, and ensure that our public directory exists.

Starter code

In our shadow-cljs.edn file, we talk about a cljs-firebase.core/cloud-functions var that doesn't exist yet. Let's take care of that.

Create the file src/cljs_firebase/core.cljs with the following code:

(ns cljs-firebase.core
  (:require ["firebase-functions" :as functions]))

(defn handle-request [^js req, ^js res]
  (.send res "Hello, world"))

(def cloud-functions
  #js{:handleRequest (.onRequest functions/https handle-request)})

Three things to note here:

  1. The string "firebase-functions" is used to require an npm module, aliased to functions
  2. We use the .onRequest function of functions/https to create a http triggered function
  3. HTTP cloud functions receive Express Request and Response objects. We use a ^js type hint, which helps with externs inference. In this case, we want to make sure that the .send function is not renamed.

Create a Firebase project

Now it's time to associate our project with a real Firebase project, which you can create for free in the Firebase Console.

Pay attention to your project id, shown below your project name: a free .web.app domain name will be set up for you based on this ID.

Once this is finished, run the following command in your terminal, and pick your newly-created project from the list. (The tool may ask you to authenticate with your Firebase account first.)

yarn firebase use --add

You will have the opportunity to choose an alias, like prod or staging, for the project you choose. Aliases make it easy to deploy your code to different projects.

By running this command with yarn, we can be sure that our shadow-cljs executable installed in node_modules will be used. If you are using npm, the equivalent command is npx, eg. npx firebase use --add

Try it locally

To run this locally, first compile our functions:

yarn shadow-cljs compile functions

This will perform a one-time compile. You could also run yarn shadow-cljs watch functions to watch the :functions build, re-compiling as source files change.

Next, let's copy our package.json to functions/package.json (Firebase functions only reads from this directory.)

cp package.json functions/package.json

Now let's start the Firebase dev server:

yarn firebase serve

If everything has been set up correctly, you should see "Hello, world" at http://localhost:5000.

Deploy!

To ship this code to the web, run:

yarn firebase deploy

This may take a couple of minutes. After your project compiles, your public directory is uploaded to Firebase servers, and your function is uploaded. At the end, a "Hosting URL" is displayed, which you can navigate to to view your new site.

Using this template

At this point, our code is a reasonable starting-point for any new Firebase functions-based project, so we've published it on Github as a template repository: appliedscience/cljs-firebase-functions. Click Use this template to make a copy for yourself and begin your own project. We look forward to seeing what you come up with!

Matt Huebert, 03 September 2019