18

This is a bit simpler a question than I tend to like to come here with but I've been driving myself up the wall trying to find an answer to this and I absolutely cannot-

Do Google Cloud Platform HTTP Functions support Route Parameters, as here? http://expressjs.com/en/guide/routing.html#route-parameters

Specifically, I see that Google Cloud Platform HTTP Functions appear to use Express as a base, yet all functions I see any example of already just run off of req and res parameters and nothing else. I can access data within the body, but that doesn't allow me to pull parameters from the route like finding the book ID in a request passed to "/users/:userId/books/:bookId". I can't see how they could be populated into req.params without the ability to specify which part of the path corresponds to which name as here.

I understand that I can always pass them in another way, but this is cleaner and more in keeping with the setup we'd like to use, so I'd really like to make it work if possible. Is there some way to do this that I'm completely missing or is this not supported at all?

6 Answers 6

15

I was able to reach out to the support group for this and it appears that yes, this is supported - you just have to use req.path in order to pull the full path and then parse it in some way (I used path-to-regexp)

Sample code:

exports.myFunction = function(req, res) {
    var keys = [];
    var re = pathToRegexp('/:paramA/:paramB/not-a-param/:paramC/also-not-a-param/:paramD?', keys, {strict: false});
    var pathVars = re.exec(req.path);
    if (pathVars) {
        console.log(JSON.stringify(pathVars));
        var paramA = pathVars[1];
        var paramB = pathVars[2];
        var paramC = pathVars[3];
        var paramD = pathVars[4];
        // Do stuff with the rest of your functionality here
        res.status(200).send('Whatever you wanna send');
    }
}

The command line code to deploy this would then look something like gcloud beta functions deploy myFunction --stage-bucket<STORAGE_BUCKET> --trigger-http (Full documentation for this command here). Your new endpoint URL will then be https://<YOUR_REGION>-<YOUR_PROJECT_ID>.cloudfunctions.net/myFunction, and you can then append subsequent query or route parameters to it when actually making your call (e.g., making a get call to https://<YOUR_REGION>-<YOUR_PROJECT_ID>.cloudfunctions.net/myFunction/paramA/paramB/not-a-param/paramC/also-not-a-param/paramD).

Please note that:

  1. Your function should be exported under the same name as used in the CLI unless you use the --entry-point flag. This name will be used in your resulting URL.
  2. The --stage-bucket <STORAGE_BUCKET> command is optional, but I've always used it.
  3. Cloud Functions will automatically look to a file named index.js or function.js to find your function, but if you provide a package.json file which contains a main entry, then Cloud Functions will look for it instead.
  4. This will, I assume, leave beta at some point, at which you should update to the new command tools.
Sign up to request clarification or add additional context in comments.

6 Comments

What does the cli deployment process look like when using route parameters?
@gregm Oh man, and I was just recently complaining about people being like "I fixed it!" without providing sample code, too... I'm a hypocrite apparently, lol. Sample code added; hopefully that should be sufficient. LMK if you have any further questions. As for Stephen Handley, I hope he got the answer to his question at some point, but for anyone else here, if I understand the question correctly, I'm not aware of any changes in CLI deployment with route parameters. Just treat it like it has none.
What wasn't clear to me in the answer is how the function is deployed in cli or console. So I tested it out: I deployed an HTTP trigger function with a name of 'helloworld', i.e., the url is /helloworld. Then the function will be called on any url nested under it. For example, '/helloworld/123/456/789.jpg` will call that function. And so within the function, you can set up your internal express routing. I wish the Google docs stated that more explicitly.
@jacob Ahh, yeah, Google documentation is a mess in my experience. I went ahead and edited my answer to have some additional CLI/Console deployment information, which I hope will help.
If somebody is coming back to this in 2020: you can directly export your Express app as your function and all the normal routing stuff will work. Here is a good blog about it: medium.com/google-cloud/…
|
2

You could try modofun: https://modofun.js.org

Which is a router for multiple operations based on the request path, and also supports automatic parameter parsing from the path.

It parses the URL path into an operation name and a list of parameters which are populated into req.params, same as with Express when using regex. So you could do something like this:

var modofun = require('modofun')

exports.bookstore = modofun(
  {
    /**
     * /users/:userId/:bookId
     */
    users: (req, res) => {
      var [userId, bookId] = req.params
      // ...
      res.json(data)
    }
  },
  { mode: 'reqres' }
)

Or, you can also have the parameters expanded into function arguments and work with pure functions, like this:

var modofun = require('modofun')

exports.bookstore = modofun(
  {
    /**
     * /users/:userId/:bookId
     */
    users: (userId, bookId) => {
      // ...
      return data
    }
  },
  { mode: 'function' }
)

I made it for a Google Cloud Functions deployment I have in production, mostly because I don't need the large dependency trail from Express, and I wanted something more lightweight.

But remember that you can also just use an Express together with Google Cloud Functions. Nothing stops you from creating an Express app with the usual route matching rules and then passing that as the export for your GCloud Function.

Hope that helps.

Comments

2

First Create a Handler function,

function bookIdHandler(req,res){
    let bookId= req.params.bookId;

    //your code goes here
}

You can just pass an Express App object to cloud function routes

const express = require('express');

const app = express();

app.get('users/:userId/:bookId', bookIdHandler);
//++ any other express endpoints


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

Ref:How its mentioned in firebase docs

Comments

1

If you don't want to pass data in the body, you can always put it into a query parameter of the url. Like:

http://yourGoogleProject.cloudfunctions.net/users?userid={userId}&bookId={bookid}

And in the cloud function you simply access the query parameter from the req object, like:

exports.users = (req, res) => {
  res.status(200).send(`Hello from user:  + $(req.query.userid) your bookId: $(req.query.bookid)`);
  }

Comments

1

Here is an example of how to implement a file-based router (similar to Next.js API router), that works with Cloud Functions and URL parameters.

Assuming you want to handle a bunch of API endpoints looking as follows:

  GET https://example.com/api/v1/posts
 POST https://example.com/api/v1/posts
  GET https://example.com/api/v1/posts/123
PATCH https://example.com/api/v1/posts/123

Create a new file for each URL route:

/v1/posts/[id].ts
/v1/posts/index.ts
/v1/authors/[username].ts
/v1/authors/index.ts

Where each route matches the following signature (example):

import { HttpFunction } from "@google-cloud/functions-framework";

/**
 * Fetches a post by ID.
 *
 * @example GET /api/v1/posts/1
 */
export const GET: HttpFunction = async (req, res) => {
  const url = `https://jsonplaceholder.typicode.com/posts/${req.params.id}`;
  const fetchRes = await fetch(url);
  const post = await fetchRes.json();
  res.send(post);
};

/**
 * Updates an existing post.
 *
 * @example PATCH /api/v1/posts/1
 */
export const PATCH: HttpFunction = async (req, res) => {
  throw new Error("Not implemented");
};

At the top level, in the entry point, just load the list of files from the "routes" folder, e.g.

// https://vitejs.dev/guide/features.html#glob-import
const routeFiles = import.meta.glob(["./v1/**/*.ts", "!./**/*.test.ts"]);

Append a RegExp for each route derived from the filename using path-to-regexp, then iterate through the files and execute the matching handler function:

import { HttpFunction, http } from "@google-cloud/functions-framework";
import { HttpError, NotFound } from "http-errors";
import { match } from "path-to-regexp";

// https://vitejs.dev/guide/features.html#glob-import
const routeFiles = import.meta.glob(["./v1/**/*.ts", "!./**/*.test.ts"]);
const routes = Object.entries(routeFiles).map(...);

http("api", async (req, res) => {
  try {
    for await (const route of routes) {
      const match = route.match(req.path);

      if (match) {
        Object.assign(req.params, match.params);
        const module = await route.import();
        await module?.[req.method]?.(req, res);
        if (res.headersSent) return;
      }
    }

    throw new NotFound();
  } catch (err) {
    // TODO: Send an error response
  }
});

A unit test for each route would look similar to this (e.g. v1/posts/[id].test.ts):

import { getTestServer } from "@google-cloud/functions-framework/testing";
import supertest from "supertest";
import { expect, test } from "vitest";
import { functionName } from "../../index";

test("GET /api/v1/posts/1", async () => {
  const server = getTestServer(functionName);
  const res = await supertest(server)
    .get("/api/v1/posts/1")
    .set("Accept", "application/json");

  expect({ statusCode: res.statusCode body: res.body })
    .toEqual({/* ... */});
});

https://github.com/koistya/cloud-functions-routing

Comments

0

Use Express API

npm install --save express body-parser

In index.ts make the following changes:

//import library
import * as express from 'express';
//init Express server
const app = express();
//change the Google function to receive an Express server object
export const myFunc = functions.https.onRequest(app);
//create your endpoints specifying HTTP Verbs
app.get('/isbn/:isbn', async (request, response) => {
   //...
   //get route parameter
   const isbn = request.params.isbn;
   //...

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.