DEV Community 👩‍💻👨‍💻

Thor Galle
Thor Galle

Posted on

Running multiple Google Cloud functions locally with the functions-framework

So, you want to run multiple node.js Google Cloud Functions locally at the same time using Google’s functions-framework?

You might have previously written cloud functions in Firebase, where the Firebase Local 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:

├── package.json
└── src
    ├── index.js
    └── functions
        ├── firstFunction.js
        └── secondFunction.js
Enter fullscreen mode Exit fullscreen mode

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.

// src/functions/firstFunction.js
export const firstFunction = async (req, res) => {
    res.redirect('/secondFunction');
}
Enter fullscreen mode Exit fullscreen mode
// src/functions/secondFunction.js
export const secondFunction = async (req, res) => {
    res.send("OK! You were redirected here.");
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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"
}
...
Enter fullscreen mode Exit fullscreen mode

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.

// 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};
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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: the effects on req.path and other Express variables

It might seem that using Express “sub apps” this way not a proper way of emulating multiple individual functions running in Google Cloud at the same time. There are some caveats.

req.path

What if you need to access req.path in your app? Doesn’t this behave differently than running a single function in Google Cloud or locally?

When you run a single function as a local function, and then call it on http://localhost:8080/ while accessing req.path in that function, it will yield /. The same holds for calling a function running in Google Cloud on https://your-google-cloud-domain/firstFunction: accessing req.path will still yield / even though the actual URL path you called was /firstFunction!

Are we replicating this behavior correctly in our local development environment? It’s important to have parity between local development setups, and production setups.

The answer is yes. See the Express documentation for 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 onto the app with the “mount point” being /firstFunction.

The omission of the mount point means that if we run the exported Express index app (== app), and we call http://localhost:8080/firstFunction, then it seems to the actual firstFunction function that it is running on http://localhost:8080/, rather than http://localhost:8080/firstFunction.

req.originalUrl ⚠️

Express Docs

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() 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 external routing that Google Cloud does.

With the above index.js hack, 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.

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 that you might have seen already when researching this issue.

I wrote this because that thread contains several approaches to this issue that were less relevant to my use-case (and also a little bit of unnecessary drama). I hope developers used to the Firebase functions model find this setup suggestion helpful.

Top comments (0)

12 Rarely Used Javascript APIs You Need

Practical examples of some unique Javascript APIs that beautifully demonstrate a practical use-case.