16

There are many examples of using Express for Firebase Cloud Functions.

In every example of I have found the code exposes the Express app as a single Cloud Function:

exports.app = functions.https.onRequest(app);

For one's Firebase Project Functions that means they will see one entry called "app" and all logs for all Express.js HTTP listeners will go to one place in Firebase. This also means, no matter how large one's Express.js app is, Firebase will deploy a single function in production for the app.

Alternatively, when using firebase-functions.https.onRequest you get separate functions for each export, for example, in Typescript:

export const hello = functions.https.onRequest(async (req, res) => {
  res.status(200).send('hello world');
});

And in Firebase Console, I have my hello function and also another function in my index.js:

enter image description here

This also means Firebase will create different nodes/instances for each function: hello and emphemeralKey.

And I will get separate logging for each function in the Firebase Console.

I would like to use middleware to ensure that valid auth tokens are being passed to my endpoint Cloud Functions like this Firebase example but I would prefer not to use a single "app" single Cloud Function, I would prefer a dedicated function for function export in my index.js.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Brian Ogden
  • 18,439
  • 10
  • 97
  • 176
  • Why does this matter to you? What's the requirement? – Doug Stevenson Feb 03 '19 at 23:14
  • @DougStevenson I explained right in my question how Firebase Cloud Functions will deploy one node for each instance and also how logging is handled. In addition, my requirements now, and in the future do not validate or negate my question. – Brian Ogden Feb 03 '19 at 23:21
  • I'm not trying to invalidate your question. I'm trying to understand what the real problem is here. So all you want is separate logging for each endpoint? I don't know what you mean by "deploy one node instance". I think you may not understand how Cloud Functions scales. – Doug Stevenson Feb 03 '19 at 23:26
  • No, I do not just want separate logging, I want separate Cloud Function node/instance created for each exposed function, otherwise I have one Node for "app" and all request endpoints in app. – Brian Ogden Feb 03 '19 at 23:28
  • What is the problem with the current deployment? A single function will scale up to 1000 instances. – Doug Stevenson Feb 03 '19 at 23:29
  • I'm not saying your question is not valid. I think you may just not understand how Cloud Functions works behind the scenes. When you deply a function, you don't get just a single server instance. It will scale up to the limits defined in the documentation. https://cloud.google.com/functions/quotas – Doug Stevenson Feb 03 '19 at 23:32
  • @DougStevenson I have removed the word "need" from my question and replaced with "prefer", chalk this question up to curiosity if you will – Brian Ogden Feb 03 '19 at 23:32
  • Need or prefer, it doesn't matter. If you have a single express app, you can't split it up between its different routes. I'm suggesting that there might be a way around whatever you perceive to be the limitations of the system. – Doug Stevenson Feb 03 '19 at 23:34
  • @DougStevenson I do not think I will have any scaling issues and I know that I will have to be doing really well $$ to hit any performance concerns. I can deploy a total of 1,000 functions for per project using firebase-functions.https. With the Express.js "design pattern", to take advantage of middleware, I can only deploy one function per Firebase project and still use the same middleware. Individual scaling of each function seems nicer to me. – Brian Ogden Feb 03 '19 at 23:40
  • If you don't know if you will be exceeding the limits of the product, as I pointed you previously, I personally think you are trying to do premature optimization of your app. I'll give you some options here, but I don't think they are worth pursuing until you actually start to observe real problems. – Doug Stevenson Feb 03 '19 at 23:44
  • @DougStevenson I thought there would be an easy way to use the nice Express.js middleware options like cors and auth/bearer token and still export a function for each route/endpoint so Firebase creates a Cloud Function for each. I didn't think it would be a challenging problem to solve. I am quite comfortable having just an "app" endpoint and have no interest in focusing a large amount of time on this question, it is part curiosity. – Brian Ogden Feb 03 '19 at 23:46
  • Are you saying you can't use cors middleware in an express app deployed to Cloud Functions? That sounds like a completely different problem. – Doug Stevenson Feb 03 '19 at 23:50
  • Well I want to follow this official Firebase guide for auth on my endpoints https://github.com/firebase/functions-samples/blob/master/authorized-https-endpoint/functions/index.js – Brian Ogden Feb 03 '19 at 23:52
  • @DougStevenson I understand too that I can create different Express instances in my functions/index.js and have multiple exports and still use same middleware – Brian Ogden Feb 03 '19 at 23:53
  • I guess I don't see how you are prevented from using cors in the way you're linking to. This sounds like a whole different question to me. – Doug Stevenson Feb 03 '19 at 23:58
  • @DougStevenson I not concerned about cors middleware, I only mentioned cors. It is the guide for Firebase auth token validation middleware that I am trying to use without neccarsily using the Express "pattern" like the example demonstrates https://github.com/firebase/functions-samples/blob/master/authorized-https-endpoint/functions/index.js – Brian Ogden Feb 04 '19 at 00:04
  • Have there been any updates or changes to Firebase which would now allow for separate Firebase Functions (as well as logs) for each route of an Express application? (e.g. GET /users would be a route in the Express application and have its own Firebase Function and respective logs) – Jonathan Wang Nov 05 '21 at 06:40

4 Answers4

5

Thanks to Doug Stevenson for his answer and help. I wanted to provide my own answer though.

So the answer to my question is, generally speaking: no you can't.

As Doug was pointing out, this is not a problem for many people's scaling needs. Firebase will create up to 1,000 instances of your function to scale.

I wanted to provide a slightly different answer then Doug's to how I would write an Express app and have different Firebase Cloud Functions for a project:

const payment = express()
const order = express()
payment.get('/route', ...)
order.get('/route', ...)
export const payment = functions.https.onRequest(payment)
export const order = functions.https.onRequest(order)

The advantage here is that I can start to express REST or RPC routes like:

  • /payment/someaction (RPC)
  • /order (get,put,post, etc.)

Another benefit is that I can provide a "test" API and a "live" API for things like credit card payments/processing:

// [START Express LIVE App]


// [START get user]
app.get('/user', async (req, res) => {
  await handleGetUser(req, res, paymentServiceLive);
});
// [END get user]

// [START claim]
app.post('/claim', async (req, res) => {
  await handleClaim(req, res, claimEmailTo);
});
// [END claim]

// [START user]
app.post('/user', async (req, res) => {
  await handleUserPost(req, res, paymentServiceLive);
});
// [END user]

// [START ephemeralKey]
app.post('/ephemeralKey', async (req, res) => {
  await handleEphemeralKey(req, res, paymentServiceLive);
});
// [END ephemeralKey]


// [START charge]
app.post('/charge', async (req, res) => {
  await handleCharge(req, res, paymentServiceLive);
});
// [END charge]

// [START purchase]
app.post('/purchase', async (req, res) => {
  await handlePurchase(req, res, paymentServiceLive);
});
// [END purchase]

//Expose Express API as a single Cloud Function:
exports.app = functions.https.onRequest(app);

// [END Express LIVE App]



// [START Express TEST App]

// [START get user]
appTest.get('/user', async (req, res) => {
  console.log('appTest /user get', req);
  await handleGetUser(req, res, paymentServiceTest);
});
// [END get user]

// [START claim]
appTest.post('/claim', async (req, res) => {
  await handleClaim(req, res, claimEmailToTest, true);
});
// [END claim]


// [START user]
appTest.post('/user', async (req, res) => {
  console.log('appTest /user post', req);
  await handleUserPost(req, res, paymentServiceTest);
});
// [END user]

// [START ephemeralKey]
appTest.post('/ephemeralKey', async (req, res) => {
  await handleEphemeralKey(req, res, paymentServiceTest)
});
// [END ephemeralKey]


// [START charge]
appTest.post('/charge', async (req, res) => {
  await handleCharge(req, res, stripeTest);
});
// [END charge]

// [START purchase]
appTest.post('/purchase', async (req, res) => {
  await handlePurchase(req, res, paymentServiceTest);
});
// [END purchase]

//Expose Express API as a single Cloud Function:np
exports.apptest = functions.https.onRequest(appTest);

// [END Express TEST App]

This allows me to have a development environment and a live environment. in my app config files I just have a different API url:

/us-central1/apptest

or

/us-central1/app
Brian Ogden
  • 18,439
  • 10
  • 97
  • 176
  • @DougStevenson do you see anything wrong with the above approach? – Brian Ogden Feb 05 '19 at 19:51
  • 4
    I came here for an answer to this question, and came to the same conclusion you did with this answer. I also see another valid reason for splitting them, which is that you can more easily view errors, performance, latency, and memory, etc, from the console, and an API level. When all of the APIs are bundled under the same function, it's harder to determine if 1 is an outlier. @DougStevenson right? Or is there another work-around – Philberg Apr 24 '19 at 16:44
  • @Philberg agreed, another benefit I use to this approach is having a test API and live API for credit card payment processing – Brian Ogden Apr 25 '19 at 01:10
  • @Philberg I just added an example of the benefit of being able to have a test and production API with this method, see the update to my answer – Brian Ogden Apr 25 '19 at 01:17
  • It seems silly that the only way to do this with express is to literally export a separate instance of express to do this. Oh well, hopefully GCF improves on this soon – Philberg Apr 25 '19 at 22:55
  • I think alternate at this poing would be to create a new Firebase project and use one for live/production and the other project for debug/test etc – Brian Ogden Apr 25 '19 at 23:11
  • For your usecase yes. I simply want to be able to have function level control per API (and to be able to see all of the stats per API call). I don't understand why that's difficult given that a full express app is being passed in – Philberg Apr 26 '19 at 04:19
4

Interesting discussion.

I choose the same approach : one "endpoint" (aka a root route like "/posts", "/users") == a dedicated cloud function (for the reason already evoked + it's more "µservice like" and it's what are "lambda functions" for me).

To be "DRY" all my function import an "express" generator. I configure my express instance in one place.

const express = () => {
  const express = require("express");
  const cors = require("cors")({ origin: true });
  const morgan = require("morgan");
  const helmet = require("helmet");

  const app = express();

  app.use(helmet());
  app.use(helmet.noCache());
  app.use(cors);
  app.use(morgan("combined"));

  return app;
};

module.exports = express;

My "hello" endpoint :

const app = require("./../../express")();

/**
 * /hello
 */

app.get("/", (req, res) => {
  return res.send("Hello World");
});

module.exports = app;

My index.js (main export) :

const helloApi = require("./api/hello");

const https = functions.region("europe-west1").https;

module.exports.hello = https.onRequest(helloApi);

Seems to work well for us :)

rphlmr
  • 838
  • 6
  • 12
3

If you have a single express app, you can't split its routes between different logical functions in a project. If you must try to split the load between more than one function, you can deploy the same express app as to as many individual functions as you want.

const app = express()
app.get('/route1', ...)
app.get('/route2', ...)
export const f1 = functions.https.onRequest(app)
export const f2 = functions.https.onRequest(app)
// etc

Now you can try to address that different routes between the different functions (and their different resulting URLs). But you have not inherently restricted certain routes from being invoked in different functions. It's up to you to make sure clients are using the function you want.

If you're trying to perform this split for performance reasons, I would consider this premature optimization. Cloud Functions will scale up your app seamlessly beyond just a single server instance, on demand. Splitting up your functions like this may help scalability, if you're expecting to exceed the documented limits, but I would expect that to be uncommon. Without understanding the actual, observed performance characteristics if your app, it's impossible to say. If you are exceeding the limits, contact support to help explain what's not happening the way you expect with your deployment.

If you find the basic logging for Cloud Functions to be unhelpful when it comes to monitoring each of your routes, you should look into custom StackDriver logging, which will help you better organize and monitor the different types of logs your functions generate.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • Right, and I can also create multiple express apps correct? const app1 = express() and const app2 = express() yes? – Brian Ogden Feb 04 '19 at 00:05
  • Sure, if that helps your situation. If you are going to have a single route per app, you may as just well use a regular old HTTP function. You can still apply cors middleware to a regular HTTP function. https://github.com/firebase/functions-samples/blob/Node-8/quickstarts/time-server/functions/index.js#L59 – Doug Stevenson Feb 04 '19 at 00:08
  • thanks for your help, I didn't realize I have been watching some of your Firebase videos lately, I thought you looked familiar :) – Brian Ogden Feb 04 '19 at 01:01
2

Just thought it was useful to leave this registered, because it worked for me... You can do stuff like this if you find it useful:

const app = express();

export const appGetUser = functions.https.onRequest(
  app.get("/users/:userId", getUser)
);

This way it's one Express app as a whole, but each API endpoint will be considered as a a separate Firebase Cloud Function. You will need to access them through something like https://.../myFirebaseProject/us-central1/appGetUser/users/{userId}.

Then, if you wish to have a less redundant endpoint:

export const users = functions.https.onRequest(
  app.get("/:userId", getUser)
);

It's gonna now be https://.../myFirebaseProject/us-central1/users/{userId}.

Philippe Fanaro
  • 6,148
  • 6
  • 38
  • 76