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:
- We’ll write a small ClojureScript function that handles http requests. It will be compiled for node.js using shadow-cljs.
- We’ll deploy this as a Firebase Cloud Function.
- We connect our function to the web using a Firebase Hosting rewrite rule.
What we like about this setup:
- Shadow CLJS has a
:node-library
target that emits exactly the sort of file we need to provide Firebase in order to deploy a Cloud Function. - Cloud Functions are billed in increments of 100ms, with a generous free tier that covers hobby usage and small services.
- Firebase Hosting makes deployment dead simple, and includes a CDN. High performance, low maintenance. We even get a free
.web.app
domain.
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:
- The string
"firebase-functions"
is used to require annpm
module, aliased tofunctions
- We use the
.onRequest
function offunctions/https
to create a http triggered function - 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
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: applied-science/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