Running multiple Google Cloud functions locally with the functions-framework
Tags: googlecloud, serverless, node, express
Published: 2022-10-27 10:47:17

So, you want to run multiple node.js Google Cloud Functions locally at the same time using Google’s [functions-framework](https://github.com/GoogleCloudPlatform/functions-framework)?

You might have previously written cloud functions in Firebase, where the [Firebase Local Emulator Suite](https://firebase.google.com/docs/emulator-suite) allowed you to run all your functions simultaneously, on a single local server, with a single command (`firebase emulators:start`).

The function framework does not provide an emulator that can do this out-of-the-box. However, **you can very easily write one yourself**, and approximate Firebase’s local development experience this way.

This approach combines your functions in a single Express “meta” app for development purposes only. You can still deploy the functions individually to Google Cloud.

## Example setup

In this example I have the following **directory structure**:

```bash
├── package.json
└── src
    ├── index.js
    └── functions
        ├── firstFunction.js
        └── secondFunction.js
```

### The function scripts

The two functions themselves are full-fledged Express.js handlers, like they would be in Firebase. 

To test that the two functions can interact, the first function returns HTTP 302 redirect, which redirects to a GET request on the second function.

```jsx
// src/functions/firstFunction.js
export const firstFunction = async (req, res) => {
	res.redirect('/secondFunction');
}
```

```jsx
// src/functions/secondFunction.js
export const secondFunction = async (req, res) => {
	res.send("OK! You were redirected here.");
}
```

### package.json

The `package.json` refers to the `src/index.js` as the main node script. We also need to tell the functions-framework to target the `index` export within the `index.js` module:

```jsx
// package.json

...
"type": "module",
// Tells the functions-framework where to look for exports.
"main": "src/index.js", 
"scripts": {
    "start": "functions-framework --target=index", // Select target export
    "debug": "functions-framework --target=index --debug"
}
...
```

### index.js

The `index.js` file is the core of this setup. It’s where we will expose all functions combined on a single local address, as well as expose functions individually.

```jsx
// src/index.js
import express from "express"
import { firstFunction } from "./functions/firstFunction.js";
import { secondFunction } from "./functions/secondFunction.js";

// Solution to expose multiple cloud functions locally
const app = express();
app.use('/firstFunction', firstFunction);
app.use('/secondFunction', secondFunction);


export {app as index, firstFunction, secondFunction};
```

The local functions-framework will target the `index` export exported from `index.js`, see the `package.json` above. The `index` export is only meant for local development purposes, so we can run multiple functions at once locally. 

We still export the individual functions too, so we can easily deploy both functions individually. See “Deploying functions individually” below.

## Running the functions together

Now if we run `npm run start`, a development server will start on `http://localhost:8080`.

Running `curl http://localhost:8080/firstFunction` will print `OK! You were redirected here.`, demonstrating that both functions are running at the same time.

If you still want to test a function in isolation, you can run `functions-framework --target=firstFunction` instead, after which you can call it as usual with `curl http://localhost:8080`.

## Deploying functions individually

Functions can still be individually deployed, with the `gcloud` CLI:

```bash
gcloud functions deploy firstFunction --project=my-project --runtime nodejs16 --trigger-http --allow-unauthenticated  --security-level=secure-always --region=eyour-region --entry-point=firstFunction --memory=128MB --timeout=60s
```

```bash
gcloud functions deploy secondFunction --project=my-project --runtime nodejs16 --trigger-http --allow-unauthenticated  --security-level=secure-always --region=your-region --entry-point=secondFunction --memory=128MB --timeout=60s
```

The key here is `--entrypoint firstFunction` flag, which is similar to `--target` flag on the `functions-framework` command. It selects the module export of the index script that should be seen as the entry point for the cloud function.

You *could* also deploy the `index` export as a single function that combines all functions in one, but then you would have call `/index/firstFunction` and `/index/secondFunction` on the cloud, and you then can’t scale or modify the function runtimes individually anymore.

---

## Caveats

Using Express “sub apps” this way is not a 100% watertight way to emulate multiple individual functions running in Google Cloud. There are some caveats.

### `req.originalUrl` ⚠️

[Express Docs](https://expressjs.com/en/5x/api.html#req.originalUrl)

> This property is much like `req.url`; however, it retains the original request URL, allowing you to rewrite `req.url` freely for internal routing purposes. For example, the “mounting” feature of [app.use()](https://expressjs.com/en/5x/api.html#app.use) will rewrite `req.url` to strip the mount point.
> 

 `req.originalUrl` *doesn’t* behave as it would in a real production Google Cloud  multi-function setup. 

On Google Cloud, `req.originalUrl` excludes the function name. This is likely due to some internal redirections that Google Cloud does.

With the index.js emulator proposed by this post, `req.originalUrl` will still *include* the function name.

Make sure that your code does not depend on the value `req.originalUrl` for some decisions. If it does, you might need to adapt this code.

### `req.path` behavior ✅

In Google Cloud, accessing `req.path` in a function running on `https://your-google-cloud-domain/firstFunction` will yield `/`, and _not_ `/firstFunction` (somewhat suprisingly).

With the above caveat, you might wonder, does this behavior also not copy to our local emulator setup?

The answer is: this behavior **remains the same**. See the Express documentation for [req.path](https://expressjs.com/en/5x/api.html#req.path):

> When called from a middleware, the **mount point is not included** in `req.path`.
> 

When we call `app.use('/firstFunction', firstFunction);`, we register `firstFunction` as [application-level middleware](https://expressjs.com/en/guide/using-middleware.html#middleware.application) onto the `app`  with the “mount point” being `/firstFunction`.

### More?

There might be other caveats I’m not aware of with this setup, but for now it suits my purposes, and the added local development convenience is worth any future complications.


## References

Closing note: this solution is based on [an answer in a related GitHub Issue thread](https://github.com/GoogleCloudPlatform/functions-framework-nodejs/issues/23#issuecomment-533586507) that you might have seen already when researching this issue.

I wrote this post because that thread contains several approaches to this issue that were less relevant to my use-case (and, the thread was also filled with more drama than necessary). I hope developers used to the Firebase functions model find this setup suggestion helpful.