Serverless Application with Firebase

By Amitai B.

Feb 23, 2018

This is a tutorial that explains step by step how to write a serverless application.

In this tutorial, I will use Angular and Firebase. From a developer’s point of view, both are so painless that it actually really fun, especially if you compare it to other serverless solutions such as AWS Lambda.

Firebase

Firebase is a mobile and web application development platform. Google acquired it, and has since grown and expanded its services.

Firebase provides the following services:

  • Authentication
  • Database
  • Storage
  • Hosting
  • Functions

In addition, it provides push notifications (FCM), analytics, logging and monitoring, and more. It integrates with other Google services quite easily.

First application with Firebase

We will create a very simple app that will demonstrate how to work with Firebase in a nutshell.

The application is called Grocelist, and it will use the following Firebase services:

  • Authentication
  • Database – Adding and removing groceries from a list.
  • Functions – updating a bank of groceries for autocomplete.

Our first version of the application will only work with Angular and Firebase without functions; after it is up and running we will add the functions.

In order to work with Firebase, we need to connect to a Firebase console and create a new project. Let’s call it “Grocelist.”

Then we will create a database for the app. We will use the new Firebase database – Firestore (go to the database configuration and provide “Cloud Firestore”).

Now it’s time to create the angular app, which is a pretty simple application with one component (app component).

ng new groceries

Add firebase & angularfire2 libraries:

npm install angularfire2 firebase --save

This will allow us to connect to Firebase almost out of the box.
We will change the following files to show our list in Firestore:

app.component.ts:

import { Component, OnInit } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from 'angularfire2/firestore';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';

interface Grocery {
  name: string;
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  groceriesCol: AngularFirestoreCollection<any>;
  groceries: any;
  name: string;

  constructor(private afs: AngularFirestore) { }

  ngOnInit() {
    this.groceriesCol = this.afs.collection('groceries');
    this.groceries = this.groceriesCol.snapshotChanges()
    .map(actions => {
      return actions.map(a => {
        const data = a.payload.doc.data() as Grocery;
        const id = a.payload.doc.id;
        return { id, data };
      });
    });
  }

  addGrocery() {
    this.afs.collection('groceries').add({'name': this.name});
    this.name = '';
  }

  delete(grocery) {
    this.afs.doc('groceries/'+grocery.id).delete();
  }
}

It is a very comprehensive code that uses AngularFirestore to register on events, add and remove items from the “groceries” collection.

app.component.html:

<div class="container">
  <div class="input">
    <input class="text" type="text" (keyup.enter)="addGrocery()" [(ngModel)]="name" name="name" placeholder="Add a Grocery...">
  </div>
  <div class="list-container">
    <div class="list" *ngFor="let grocery of groceries | async">
      <div class="item" (click)="delete(grocery)">
        <div >{{ grocery.data.name}}</div>
      </div>
    </div>
  </div>
</div>

Add, delete and show the list of items.

app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { environment } from '../environments/environment';

import { AngularFireModule } from 'angularfire2';
import { AngularFirestoreModule } from 'angularfire2/firestore';
import { FormsModule } from '@angular/forms';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AngularFireModule.initializeApp(environment.firebase, 'grocelist'),
    AngularFirestoreModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Add AngularFirestoreModule, and initialize it. In order to give it the configuration of our Firebase project, we add it to the environment file with the credentials from Firebase (project overview -> Add Firebase to your web app -> copy the config:

environment.ts (replace with your config variables):

export const environment = {
  production: false,
  firebase: {
    apiKey: "XXX",
    authDomain: "grocelist-XXX.firebaseapp.com",
    databaseURL: "https://grocelist-XXXX.firebaseio.com",
    projectId: "grocelist-XXXX",
    storageBucket: "grocelist-XXXX.appspot.com",
    messagingSenderId: "XXXXX"
  }
};

Then we execute

ng serve

And we get a running application with Firebase as a backend; and we are running serverless!

Adding authentication

Firebase gives us authentication out of the box, and supports several authentication methods via email/password, Facebook, Google, Twitter, etc. For our purposes, we will use Google.
Just go to the “Authentication” section and select “set up sign-in method” and choose Google, then hit enable.

We will update our code to use authentication:

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { environment } from '../environments/environment';

import { AngularFireModule } from 'angularfire2';
import { AngularFirestoreModule } from 'angularfire2/firestore';
→> import { AngularFireAuth } from 'angularfire2/auth';
import { FormsModule } from '@angular/forms';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AngularFireModule.initializeApp(environment.firebase, 'grocelist'),
    AngularFirestoreModule,
    FormsModule
  ],
  → providers: [AngularFireAuth],
  bootstrap: [AppComponent]
})
export class AppModule { }

Again we are using the out-of-box features of “angularfire2” instead of implementing them ourselves.

app.component.ts

import { Component, OnInit } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from 'angularfire2/firestore';
import { AngularFireAuth } from 'angularfire2/auth';
import * as firebase from 'firebase/app';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';

interface Grocery {
  name: string;
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  groceriesCol: AngularFirestoreCollection<any>;
  groceries: any;
  name: string;

  constructor(private afs: AngularFirestore, public afAuth: AngularFireAuth) { }

  ngOnInit() {
    this.groceriesCol = this.afs.collection('groceries');
    this.groceries = this.groceriesCol.snapshotChanges()
    .map(actions => {
      return actions.map(a => {
        const data = a.payload.doc.data() as Grocery;
        const id = a.payload.doc.id;
        return { id, data };
      });
    });
  }

  addGrocery() {
    this.afs.collection('groceries').add({'name': this.name});
    this.name = '';
  }

  delete(grocery) {
    this.afs.doc('groceries/'+grocery.id).delete();
  }

  login() {
    this.afAuth.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider());
  }
  logout() {
    this.afAuth.auth.signOut();
  }
}

app.component.html

<div class="container">
  <div *ngIf="afAuth.authState | async as user; else showLogin">
    <div class="header">
      <div class="caption">Hi {{ user.displayName }}!</div>
      <a href="#" (click)="logout()">Logout</a>
    </div>
    <div class="input">
      <input class="text" type="text" (keyup.enter)="addGrocery()" [(ngModel)]="name" name="name" placeholder="Add a Grocery...">
    </div>
    <div class="list-container">
      <div class="list" *ngFor="let grocery of groceries | async">
        <div class="item" (click)="delete(grocery)">
          <div>{{ grocery.data.name}}</div>
        </div>
      </div>
    </div>
  </div>
  <ng-template #showLogin>
    <p>Please login.</p>
    <a href="#" (click)="login()">Lo gin with Google</a>
  </ng-template>
</div>

And now we have an authentication and login for our application, without almost any work. We can use the user data any way we like, but for this example, I will use it only to display the user name.

Notice that in the real application it is better to write the authentication code in a dedicated service, but here for simplicity, it is in the component.

Using functions

We have a great web application with database and authentication. It may be enough for applications, but what if we would like to add some logic to the server?

Firebase functions are here to serve us. Firebase functions are built in on top of Google cloud functions and we have the Firebase tools to facilitate the deployment of them to the cloud.

As I mentioned before, FaaS (Function as a service) are event-driven. Firebase functions are triggered by the following events:

  • Cloud Firestore Triggers – onCreate, onUpdate, onDelete, onWrite.
  • Realtime Database Triggers – same as Firestore
  • Firebase Authentication Triggers
  • Google Analytics for Firebase Triggers
  • Crashlytics Triggers
  • Cloud Storage Triggers
  • Cloud Pub/Sub Triggers
  • HTTP Triggers

As you can see, it covers almost every common scenario of a typical application. Of course, it only works with Firebase services. So you cannot invoke them from other places such as AWS, etc.

Writing the function can be done only in JavaScript or TypeScript. In AWS Lambda, for instance, the selection is wider: JavaScript, Python, Java, C# and Go.

The best way to deploy the functions to Firebase is to use the Firebase SDK.
You simply write your functions and let the SDK deploy it for you.

npm install -g firebase-tools
firebase login
firebase init  # pay attention to choose ‘dist’ as public folder

You will have all your projects to choose from; select Groelist.
In the generated code, open index.html and add the functions there.

Adding to the global items list

We will write a function that will add any item to a global collection that will be used for suggestions. We will use the onCreate method on Firestore:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

exports.addToBank = functions.firestore.document('/groceries/{documentId}')
.onCreate(event => {
  console.log('grocery created', event.data.data());
  const groceryName = event.data.data().name;
  const bankRef = admin.firestore().collection('bank_of_groceries');
  const query = bankRef.where("name", "==", groceryName);
  query.get().then(querySnapshot => {
    if (querySnapshot.empty) {
      console.log('Add to bank', groceryName);
      bankRef.add({name: groceryName})
    }  
  })
});

The method addToBank takes the newly created item and adds it to the bank_of_groceries if it is not already there.

I added a type ahead functionality to the client so the user can choose from the previous options.

In order to deploy:

ng build --prod
firebase deploy

Your app and functions are deployed to firebase.

You can see here a live demo.

All the code is available here

Conclusion

Writing an application with Firebase is very fast and easy. The installation and configuration that we need to do it require little effort.

In just a few minutes, we have a server that is scalable, stable and reliable.
Firebase hosts all the services that we need: database, functions, authentication and more.

All we have to do is write awesome applications!

Leave a Reply

Your email address will not be published.