Serverless Pathom with GCF

In this tutorial, we are going to implement a Pathom server and deploy it as a Google Cloud Function.

We will start with the code created at the Pathom Tutorial, and make it available as a service.

Project Setup

To make the GCP integration I'll use the library google-cloud-functions-ring-adapter.

Example setup for deps.edn:

{:paths
["src/main" "src/java"]
:deps
{com.wsscode/pathom3 {:mvn/version "2021.07.10-alpha"}
metosin/muuntaja {:mvn/version "0.6.8"}
nl.epij/google-cloud-functions-ring-adapter {:mvn/version "0.1.0"}
org.clojure/clojure {:mvn/version "1.10.3"}
org.clojure/core.async {:mvn/version "1.3.618"}
ring-cors/ring-cors {:mvn/version "0.1.13"}}
:aliases
{:assemble
{:extra-deps {nl.epij.gcf/deploy {:git/url "https://github.com/pepijn/google-cloud-functions-clojure"
:sha "e0f49db974a8f97e1459efd16c0edfb3030a6115"
:deps/root "deploy"}}
:exec-fn nl.epij.gcf.deploy/assemble-jar!
:exec-args {:nl.epij.gcf/entrypoint PathomServer
:nl.epij.gcf/java-paths ["src/java"]
:nl.epij.gcf/compile-path "target/classes"
:nl.epij.gcf/jar-path "target/artifacts/application.jar"}}
:run
{:extra-deps {nl.epij.gcf/deploy {:git/url "https://github.com/pepijn/google-cloud-functions-clojure"
:sha "e0f49db974a8f97e1459efd16c0edfb3030a6115"
:deps/root "deploy"}}
:exec-fn nl.epij.gcf.deploy/run-server!
:exec-args {:nl.epij.gcf/entrypoint PathomServer
:nl.epij.gcf/java-paths ["src/java"]
:nl.epij.gcf/compile-path "target/classes"
:nl.epij.gcf/jar-path "target/artifacts/application.jar"}}}}

Recommended Settings for Pathom

Here are some recommended settings to do when exposing a Pathom API.

Cache plan results

It's common for clients to make the same EQL requests. Pathom can leverage this and persist the planning part of the process across requests. This is how we set it up:

; create a var to store the cache
(defonce plan-cache* (atom {}))
(def env
; persistent plan cache
(-> {::pcp/plan-cache* plan-cache*}
(pci/register
[ip->lat-long
latlong->woeid
woeid->temperature])))

Check the Cache page for more details on how to control the cache.

Ring handler setup

Now let's set up a Ring handler. The two main things for this:

  1. Setup content negotiation to decode/encode data, I'll use muuntaja.
  2. Handle the request using the Pathom Boundary Interface.
(ns com.wsscode.pathom-server
(:require
; to include the env setup from the Tutorial demo
[com.wsscode.pathom3.demos.ip-weather :refer [env]]
[com.wsscode.pathom3.connect.operation.transit :as pcot]
[com.wsscode.pathom3.interface.eql :as p.eql]
[muuntaja.core :as muuntaja]
[muuntaja.middleware :as middleware]))
; create a boundary interface
(def pathom (p.eql/boundary-interface env))
(defn handler [{:keys [body-params]}]
{:status 200
:body (pathom body-params)})
(def muuntaja-options
(update-in
muuntaja/default-options
[:formats "application/transit+json"]
; in this part we setup the read and write handlers for Pathom resolvers and mutations
merge {:decoder-opts {:handlers pcot/read-handlers}
:encoder-opts {:handlers pcot/write-handlers
; write-meta is required if you wanna see execution stats on Pathom Viz
:transform t/write-meta}}))
(def app
(-> handler
(middleware/wrap-format muuntaja-options)))

The :transform t/write-meta will make transit encode also the meta-data. This means the running status data will flow. It allows Pathom Viz to show debug information. Keep in mind the "run status" data is usually much larger than the response itself.

You can mitigate this in two different levels at Pathom:

  1. Set :com.wsscode.pathom3.connect.runner/omit-run-stats-resolver-io? true to remove input/output details. This adds a great reduction in the size of the status.
  2. Set :com.wsscode.pathom3.connect.runner/omit-run-stats? true to remove all status from meta
tip

This same handler setup works with any other ring server, like Pedestal, http-kit, Compojure, etc...

The boundary interface isn't required, but it gives the clients extra capabilities like allowing the user to provide root data.

Now to hook that, we need to create a Java file in our sources that will link our handler:

import nl.epij.gcf.RingHttpFunction;
public class PathomServer extends RingHttpFunction {
public String getHandler() {
return "com.wsscode.pathom-server/app";
}
}

After this part, we can test our server locally:

PORT=13337 clojure -X:run

Once it runs, we can test it by sending a request:

curl --location --request POST 'http://localhost:13337' \
--header 'Content-Type: application/edn' \
--header 'Accept: application/edn' \
--data-raw '{:pathom/eql [:temperature], :pathom/entity {:ip "198.29.213.3"}}'
# => {:temperature 26.064999999999998}
important

I used EDN fore readability, but you should use application/transit+json (with transit data) for performance and to support the custom handlers for resolvers and mutations.

GCF Deploy

For the next steps, you need to have GCP Account and install the Google Cloud SDK.

tip

On Mac, you can install GCP with brew install google-cloud-sdk

To deploy the handler as Google Cloud Function; first we assemble the jar:

clojure -X:assemble

Deploy to GCP (tune as you see fit):

gcloud functions deploy --runtime java11 --source target/artifacts/ --max-instances 1 development-pathom-server-demo --trigger-http --allow-unauthenticated --entry-point PathomServer --memory 2GB --timeout 270

If you go through with no errors, the function should be online!

note

You can find out the URL to try it in the message output after deploy. Look for httpsTrigger: ... url:

You can find the full sources of this demo at https://github.com/wilkerlucio/pathom3-demo-gcf.