Real World App - Part 12: Cloud functions for Firebase

Akshay Nihalaney
Real World Full Stack
12 min readJun 12, 2017

--

Serverless Architecture and Google Cloud Functions for Firebase

This is Part 12 of our Real World Angular series. For other parts, see links at the bottom of this post or go to https://blog.realworldfullstack.io/. The app is still in development. Check it out @ https://bitwiser.io.

Recap

In the prior part we started building out our game mechanics. In order to fetch Questions for the game, that met the criteria and was randomized, it was no longer enough to do a fetch directly from the database. We needed server-side logic to do this.

In this part, we’ll introduce server-side functions without having to build out our own server infrastructure and leverage cloud based functions for our middle-tier.

What is Serverless Architecture?

Serverless computing does not mean that a server does not exist, but unlike traditional architectures, it runs in stateless compute containers that are event-triggered, may only last for a single invocation, and fully managed by a cloud provider.

Although a relatively new paradigm a lot has been written about it. Please see the references section of the post to learn more about it.

We’ve already been using Firebase which is a Backend as a Service (BaaS) that provides us with a real-time database and an authentication service.

In this part, we’ll see how we can use Function as a Service (FaaS) to write server-side logic for our application.

The key advantage of using such an architecture is that we don’t need to spin up our server infrastructure, install any software or deploy anything besides the business functionality needed. The pay-as-you go model allows us to use the compute engine of the cloud provider on usage basis and can scale up or down as needed without the need for us maintaining all of the infrastructure.

Google Could along with Microsoft’s Azure and Amazon Web Services are the 3 big cloud providers that constantly compete in features and prices for their services. While we could use any of the 3, using Google as our provider is more of a natural fit for us due to it’s integration with Firebase that’ll reduce our setup time.

Google Cloud Functions

Google Cloud Functions is their version of serverless execution environment for building and connecting cloud services.

Cloud Functions are written in Javascript and execute in a Node.js v6.9.1 environment on Google Cloud Platform. We can take our Cloud Function written in any standard Node.js runtime and deploy it to their cloud. Read more here - https://cloud.google.com/functions/docs/concepts/overview.

Cloud Functions are triggered on certain events, such as Http request, Cloud storage object change, Pub/Sub notification etc. Read more - https://cloud.google.com/functions/docs/concepts/events-triggers

Cloud functions for Firebase

In addition to their general cloud functions, Google also released their Cloud functions specific to their Firebase platform in March 2017 (beta release) - https://firebase.google.com/docs/functions/.

Cloud functions for Firebase provides additional triggers on database read/writes and Firebase authentication triggers. Since they are part of the same Google project as your database, they also come with rights to read, write and admin rights to the database without the need for providing additional authentication mechanism.

Function Triggers

Firebase functions can be invoked on any of the following triggers -

  • Realtime Database Triggers - A function can be invoked on a database supports write event, which triggers anytime data is created, destroyed, or changed in a specified database location. A snapshot of before and after of the data is provided with the event data. This can be useful for any number of post-processing steps for the data.

In our application, when a Question is approved we actually add the Question to the Published list and remove it from the Unpublished list. This removal from the Unpublished list can be done as part of the trigger. There are numerous other use cases for such triggers like computing stats or sending email based on a change in data.

  • Firebase Authentication Triggers - A function can be triggered in response to the creation and deletion of user accounts via Firebase Authentication. For example, you could send a welcome email to a user who has just created an account in your app.
  • HTTP Triggers - We can trigger a function through an HTTP request. This will allow us to invoke a synchronous function through the following supported HTTP methods: GET, POST, PUT, DELETE, and OPTIONS.

Other trigger mechanisms can be found here - https://firebase.google.com/docs/functions/. We will start with the HTTP trigger in this part, but will certainly make use of the other trigger types later in the series.

Continue where we left off …

Before we deep dive into writing our functions, let’s upgrade our firebase and angularfire2 dependencies. In package.json -

...
"angularfire2": "4.0.0-rc.0",
"firebase": "4.0.0",
...

There are several breaking changes in angularfire2 4.0 update.The most significant one is that they’ve separated out the Database and the Authentication modules making them higher order modules, instead of child of the AngularFire module. For complete details, see the changelog - https://github.com/angular/angularfire2/blob/master/CHANGELOG.md

This also impacts the way we reference the database and the auth objects in our modules (instead of referring them as ‘this.af.auth’, we need to refer them directly as a higher level object as ‘this.afAuth’). Our unite test mocks also need to be changed appropriately. For complete code changes, refer to the github repo - https://github.com/anihalaney/rwa-trivia/commit/918f68e898f9f4a18a16989bf9203d60be47610f

Firebase functions

In order to get started with firebase functions you need to install the firebase tools. If you’re new to this series and haven’t done so already, you can get them now -

npm install -g firebase-tools

To enable functions for your project, use the following command (If this is the first time using firebase on your machine, you’ll need to run “firebase login” to authenticate to your google account) -

firebase init functions

You’ll be prompted to select your Google project. Once you follow thru the prompts, it’ll create a new folder named “functions” in your project root. It’ll also prompt you to install the dependencies, choose “Y” to install them.

Take a look at functions folder. It just has an index.js and a package.json file. The package.json has 2 dependencies “firebase-admin” & “firebase-functions”.

Let’s write a sample function using this guide - https://firebase.google.com/docs/functions/get-started

index.js -

const functions = require('firebase-functions'); 
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.addMessage = functions.https.onRequest((req, res) => {
const original = req.query.text;
admin.database().ref('/messages').push({original: original}).then(snapshot => {
res.redirect(303, snapshot.ref);
});
});

And we’ll deploy and test it out -

firebase deploy --only functions

The above command will deploy the firebase function on to the google cloud and will provide you with a url to the function (example URL: https://us-central1-<app-name>.cloudfunctions.net/addMessage?text=testing).

You can test the url (google will authenticate the user) by passing in the query string parameter to the url “?text=testing” and see that the message gets written to the /messages node in you firebase database.

Firebase console

We can check our deployed functions @ https://console.firebase.google.com and selecting Functions on the navigation of our Google Project. The Dashboard will list out functions, while the logs will provide an event log of function calls as well as any “console.log” in your code. The Usage tab provides usage info useful for analyzing the number of calls to the functions as well as for billing info.

Http Trigger

Let’s take a moment to inspect the signature of the function above - functions.https.onRequest((req, res) => {})

Google functions follow the syntax -

functions.<trigger>.<optional object>.<event name>((<event data> => {…})

In our above example the trigger is “https” and the event name is onRequest with req and res as event parameters.

Functions in Typescript

The above index.js is in JavaScript. While we can continue to develop this in JavaScript, I personally prefer to write in Typescript. Let’s add tsconfig to the functions folder -

{
"compilerOptions": {
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"target": "es6",
"lib": [
"es2016"
],
"typeRoots": [
"node_modules/@types"
],
"module": "commonjs",
"types": ["node"]
},
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}

Next, we would add @types/node to our dev dependencies in functions/package.json and do an npm install on functions folder -

"devDependencies": {
"@types/node": "^7.0.21"
}

For easier compilation and deployment, we’d then add commands to our project’s root package.json -

"scripts": {
...
"compile-functions": "tsc --project functions",
"deploy-functions": "tsc --project functions && firebase deploy --only functions"
},

We’ll then rename our index.js to index.ts and convert our javascript to typescript (see source on the github repo)

Let’s deploy this again

npm run deploy-functions

Functions middleware

The above function runs on the server and can only be invoked by the Firebase project’s administrators and not the end-users.

In order to provide functionality to the end users, we need to expose functions to the outside world and then secure them with end-user authentication. In addition, we need our middleware to have cors support so they can be called from our front-end clients.

As we provide more functionality thru functions, we would need different end-points for different pieces of functionality, each of them requiring some common pre/post processing such as authentication and cors support mentioned above.

In order to enable this, we’ll add Express middleware to our functions. If you’re new to Express, read more here - https://expressjs.com/. Express will provide the necessary middleware and routing support (for various end-points) for our functions.

Authenticating end-users with Firebase functions

When a user logs in to our app, the Firebase authentication service returns an access token. Subsequent requests are made with this access token in the header and is parsed on the server to grant/deny access to resources (currently used by our instance of the Firebase real-time database) on the server. We would use the same token to identify the user in our functions middleware.

Let’s put this together. We’ll add the required packages to functions/package.json and npm install them -

"dependencies": {
...
"express":"4.15.3",
"cookie-parser":"1.4.3",
"cors":"2.8.3"
}, ...

Modify index.ts -

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const express = require('express');
const cookieParser = require('cookie-parser')();
const cors = require('cors')({origin: true});
const app = express();
const validateFirebaseIdToken = (req, res, next) => {
console.log('Check if request is authorized with Firebase ID token');
if ((!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) &&
!req.cookies.__session) {
console.error('No Firebase ID token was passed as a Bearer token in the Authorization header.',
'Make sure you authorize your request by providing the following HTTP header:',
'Authorization: Bearer <Firebase ID Token>',
'or by passing a "__session" cookie.');
res.status(403).send('Unauthorized');
return;
}
let idToken;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
console.log('Found "Authorization" header');
// Read the ID Token from the Authorization header.
idToken = req.headers.authorization.split('Bearer ')[1];
} else {
console.log('Found "__session" cookie');
// Read the ID Token from cookie.
idToken = req.cookies.__session;
}
admin.auth().verifyIdToken(idToken).then(decodedIdToken => {
console.log('ID Token correctly decoded', decodedIdToken);
req.user = decodedIdToken;
next();
}).catch(error => {
console.error('Error while verifying Firebase ID token:', error);
res.status(403).send('Unauthorized');
});
};
app.use(cors);
app.use(cookieParser);
app.use(validateFirebaseIdToken);
app.get('/hello', (req, res) => {
res.send(`Hello ${req.user.email}`);
});
exports.app = functions.https.onRequest(app);

The code is borrowed from this repo - https://github.com/firebase/functions-samples, which also has several other useful Firebase samples.

We can deploy this to the cloud. Source code for the commit.

Testing Authorization using Postman

Before we modify our Angular code, let’s test the function using an access token. In order to test our APIs I would use Postman, a Chrome extension (https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=en). Postman is a tool to send/receive http requests & responses.

In order to test our function, we would need to get the access token that’s generated when we login to our Angular app. To get this, we navigate to our web app, open the Chrome Dev Tools and then login to the app (logout if needed). You’ll see that the “verifyAssertion” request returns a response object with “idToken” (along with bunch of other info). This “idToken” is the access token we would need to test our authorization service. We can use this token in our “Authorization: Bearer <token>” header to test our service.

getNextQuestion function

The input to our getNextQuestion function would be the player and the game being played. The function can then fetch a random Question from the database that matched the game criteria, making sure that the Question isn’t repeated for the player for that game. Although we don’t have all the pieces to achieve this functionality yet, we’ll see how far we can get to in this part.

Basic getNextQuestion function -

import { Game, Question } from '../src/app/model';...app.get('/getNextQuestion/:gameId', validateFirebaseIdToken, (req, res, next) => {  let userId = req.user.uid;
let gameId = req.params.gameId;
let resp = `Hello ${req.user.email} - ${req.params.gameId}`;
let game: Game;
admin.database().ref("/games/" + gameId).once("value").then(g => {
if (!g.exists()) {
//game not found
res.status(404).send('Game not found');
return;
}
game = Game.getViewModel(g.val());
console.log(game);
resp += " - Game Found !!!"
if (game.playerIds.indexOf(userId) < 0) {
//user not part of this game
res.status(403).send('User not part of this game');
return;
}
if (game.gameOver) {
//gameOver
res.status(403).send('Game over. No more Questions');
return;
}
if (game.gameOptions.gameMode !== 0) {
//Multiplayer mode - check whose turn it is. Not yet implemented
res.status(501).send('Wait for your turn. Not yet implemented.');
return;
}
admin.database().ref("/questions/published").orderByKey().limitToLast(1).once("value").then(qs => {
qs.forEach(q => {
let question: Question = q.val();
question.id = q.key;
res.send(question);
return;
})
return;
})
.catch(error => {
res.status(500).send('Failed to get Q');
return;
});
})
.catch(error => {
res.status(500).send('Uncaught Error');
return;
});

});

The function above expects gameId as a parameter. It receives the user info from the token parsed in. We fetch the game from the db, then validate the user being part of the game and the game being still active. If all validations pass, we will return a Question.

The Question returned above is still a static Q. In order to return a question that is from a given list of categories, we would need to re-org our db to have a list of Questions under Categories. Even if we do this we won’t be easily able to get a Question randomly from these categories. If we somehow managed to achieve this getting random Qs based on tags and priority of tags will be virtually impossible from our Firebase structure.

In order to get our next Question for the game we need a search engine that is able to index the Qs based on Categories and Tags and then retrieve the Question based on the search criteria as needed. We’ll defer this to the next part of the series. For now we’ll modify our Angular code to fetch the Question using the above function.

Before we do this, notice the first statement “import { Game, Question } from ‘../src/app/model’;” in the code above. We’re now sharing our model between our front-end code and our server side function. In order to properly transpile this with Typescript to be deployable as nodejs function, we’ll re-organize our code a bit.

Let’s rename index.ts to app.ts. Then we’ll create a new file index.js that simply exports functions from this file -

//functions/index.js
exports.app = require('./server/functions/app').app;

And finally, let’s modify our tsconfig to output our transpiled code to the server folder.

...
"outDir": "./server",
...

We can now deploy our new function to Firebase and test it out using Postman. Source code here.

Angular code

We need to modify our getNextQuestion function in game.service.ts to call our newly deployed cloud function. Since the server requires an access token, we must first get this before we can make a call to our service.

We’ll first add a new property idToken to our user model. Then, let’s modify our authentication.service.ts to get the idToken -

this.afAuth.authState.subscribe(afUser => {
if(afUser) {
// user logged in
let user = new User(afUser);
afUser.getIdToken(false).then((token) => {
user.idToken = token;
this.store.dispatch(this.userActions.loginSuccess(user));
});
if (this.dialogRef)
this.dialogRef.close();
}
else {
this.store.dispatch(this.userActions.logoff());
}
});
}

Note: getIdToken is a thenable promise that’ll get us the access token.

And finally, we modify our game.service.ts to get our Question from the server -

getNextQuestion(game: Game, user: User): Observable<Question> {
let url: string = "https://us-central1-rwa-trivia.cloudfunctions.net/app/getNextQuestion/";
let headers = new Headers({'Authorization': 'Bearer ' + user.idToken});
return this.http.get(url + game.gameId, {"headers": headers})
.map(res => res.json());
}

Let’s test this out. Final code for this part - https://github.com/anihalaney/rwa-trivia/tree/part-12.

Coming up

In this part, we introduced a server side cloud functions for our app and saw how we could deploy and invoke it in a secured way.

The next part of this series we’ll index and query our Questions database to give us randomized Question we’re looking for. We’ll see how we can implement this search using Elastic Search on the cloud platform.

If you enjoyed this article please recommend and share and feel free to provide your feedback on the comments section.

--

--