Skip to content

Instantly share code, notes, and snippets.

@WendySanarwanto
Last active October 24, 2018 07:09
Show Gist options
  • Save WendySanarwanto/fd235251e16790ceed205bf29dc76d04 to your computer and use it in GitHub Desktop.
Save WendySanarwanto/fd235251e16790ceed205bf29dc76d04 to your computer and use it in GitHub Desktop.
firebase-development-flow.md

Firebase development flow

Preparation

  • Ensure that you have installed Node.js in your machine.

  • Install firebase tools CLI through running npm i -g firebase-tools command.

Firebase Function Development flow

This section covers development flow of Firebase Function. As the sample case, we are going to create functions which do CRUD actions into Firestore collections. Then, we are going to secure the Functions so that only users that have been authenticated by Firebase Authenticate are allowed to invoke the Functions.

Prepare the firebase project

  • Login into your firebase account through invoking firebase login --interactive command.

  • Create a new project direcotry. Example: mkdir firebase-demo.

  • Change current directory's location into the created node.js project's directory, and then run firebase init functions command to initialise the project with Firebase Functions dependencies and initial code files.

  • From the project's directory, change current directory's location into functions sub directory and run npm i to install firebse-admin & firebase-functions npm libraries.

  • Go back to your web browser, browse to Fireabse Console web page, do login by using your Gmail account then create a new project through clicking Add project button on the page.

  • On the shown pop up dialog, enter a unique project name, such as <your name/your team's name-<project-name>. Example: wendysa-firebase-demo, then click Create Project button.

  • Once the new project has been created, copy the project's name, as we are going to re-use it on next steps.

  • Go back to your console terminal with current directory is at new project's root directory, run firebase use --add command. Confirm that command prompt show and suggest you with a list of available projects in your Firebase account.

  • On the available Firebase projects list, select the project that we've created then hit Enter Key.

  • On the next What alias do you want to use for this project?, give it appropriate name or just left it as default. Example: development , staging, etc. Then press Enter Key to finish this phase. Confirm that there is a file named as .firebaserc that is created in the Firebase project's root directory.

Modifying the application's main entry logic as an Express application.

  • On the console terminal, change current directory's location to the Project's functions directory, then install express library: npm i express --save.

  • Rename or delete current functions/src/index.ts file and create a new index.ts in functions/src directory.

  • By using code editor such as vim or visual studio code, open the new index.ts file and add these following code:

// --- Filename: functions/src/index.ts

import * as express from 'express';
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';

// Initialise Firebase app & suppress a default warning when accessing firestore later.
admin.initializeApp();    
const firestore: admin.firestore.Firestore = admin.firestore();
firestore.settings({ timestampsInSnapshots: true });

// Create express application 
const app = express();

// TODO: Initialise required express middlewares

// TODO: Add more required routes

// Expose the Express API Routes as functions
export const api = functions.https.onRequest(app);

Creating Firestore Database

  • Back to your browser, open your Firebase project, on the Firebase web console. On the Firebase web console page, click Database menu, located on left-hand side navigation menu, under Develop section.

  • On the page's main section ("Cloud Firestore"), click Create Database button. Confirm that the Security rules for Cloud Firestore dialog appears.

  • On the Security rules for Cloud Firestore dialog, select Start in test mode option and then click Enable button. Since we are going to create the database for development activity, at this phase we just left it as accessible by everyone. Later, we are going to enforce access rule, once we are going to implement acess authorisation on database & APIs.

Implement first API

  • As for the 1st API, we are going to create a POST API which does inserting a Product Item record into firestore. As for the 1st step of this phase, create a new folder under src/functions directory, name is as items or products. This is a good practice to group all code files related (e.g. routes, services, etc) with a specific Domain or an Application in a separate folder for improving code's maintainability in future.

  • Inside the new folder, create a new express route file in Typescript with appropriate name (e.g. item.routes.ts). Inside the routes file, we define the Route of Items POST API with minimum implementation 1st, as shown in this following code:

import * as express from 'express';
import { ItemService } from './item.service';

export const ItemsRouter = express.Router();
ItemsRouter.post("/", async (req, res) => {
  let response = null;
  try{
    // Grab the request's body
    console.log(`[DEBUG] - <items.routes.addItem> req.body: \n`, req.body);

    // TODO: Instantiate a service class which handles operation related to Item domain

    // TODO: Invoke the service class method to process the request
    response = {};

	// TODO: Return the response to the caller
    res.status(201).json(response);
  } catch(error) {
    console.log('[ERROR] - <items.routes.addItem>. Details: \n', error);
    // TODO: Fill the response with error info 
    res.sendStatus(500).json(response);
  }
});

Next, we'll add several of Typescript code files which implements: the actual logic of this API, the code which interacts with Firestore and interface declarations which defines the contract of response object involved in this API's call flows.

Define response contract interface

  • 1st, we are going to create a new folder under src, name it as shared.

  • Inside the shared folder, create a new Typescript file and name it as response.ts.

  • Inside the file, write this following code, to define the Response contract interface along with the error contract interface as well:

// Filename: src/shared/response.ts

export interface ErrorInfo {
  code: string;
  message: string;
}

export interface Response {
  result: string | object;
  error?: ErrorInfo;
}

Notice in these interfaces, the Response type has result field which can be fitted with string or object value. It also has an optional error property which has ErrorInfo type. By sealing the service & http's response with these interfaces, we have enforced consistent response's contract across multiple domains within the Firebase Functions project, and also on the Angular Client as well.

  • Next, we'll need to expose these types within src/shared/index.ts file so that any places who'd like to import them, will not need to import each of these type's filename individually.
// Filename: src/shared/index.ts

export * from './response';

Create the Service file with minimum implementation

  • Create a new Typescript file in the domain folder and name it with an appropriate service name (e.g. src/items/item.service.ts).

  • Within the new file, add this following code which implement the service to create a new record with minimum implementation. Some lines are marked with TODO comments and we are going to revisit them back, once other required components are implemented & covered on next sections.

// Filename: src/items/item.service.ts

import * as admin from 'firebase-admin';

import { Response } from '../shared/response';

export class ItemService {
  async addItem(newItem = null): Promise<Response>{
    // NOTE: Should add more detailed validations
    if (newItem === null) { 
      return null;
    }

    // TODO: Create the Item repository instance then call it's record creation method which takes the newItem argument.
      
    return { result: `Not implemented` };
  }
}

Add model contract and reference it on the Service method's argument

Although passing anynomous type on service's method does not raise errors, it is a good practice in Typescript to seal it with a contract interface to safe guard the argument from being fitted with any unwanted data. Also, it is a good place to define fields that are going to be stored into Firestore as well. This type of contract interface is called Model Contract.

  • In this step, we'll create it as a new Typescript file in src/items folder and give it an appropriate name: src/items/item.model.ts.

  • Within the model contract file, implement this following code to define the Record's fields:

// Filename: src/items/item.model.ts

import * as admin from 'firebase-admin';

export interface Item extends admin.firestore.DocumentData {
  id?: string;
  name: string;
  quantity: number;
  price: number;
}
  • Go back to the src/items/item.service.ts file, and mark any item arguments to be typed as Item interface:
// Filename: src/items/item.service.ts

import * as admin from 'firebase-admin';

import { Response } from '../shared/response';
import { Item } from './item.model';

export class ItemService {
  async addItem(newItem: Item  = null): Promise<Response>{
    // NOTE: Should add more detailed validations
    if (newItem === null) { 
      return null;
    }

    // TODO: Create the Item repository instance then call it's record creation method which takes the newItem argument.
      
    return { result: `Not implemented` };
  }
}

Implement Firebase Repository class and reference it inside the Service's method

Althought it is possible to put the logic which is responsible for storing data to firestore within the service class, it is a good practice to seperate this code into somewhere else. One of design pattern that can be used to implement the separate data logic is Repository pattern. By using this pattern, data logic code are separated and isolated outside the main service's logic. Thus, it could open opportunities to use multiple Data store types in future (as needed) and ease unit testing the data logic and service's logic, as well. Below are steps to create the required Repository class:

  • Create a new Typescript file in the src/items folder and give it an appropriate name such as item.repository.ts.

  • Within the repository class file, add these following code which does inserting the passed in JSON object as a new record into Firestore Database:

// Filename: src/items/item.repository.ts

import { Item } from './item.model';
import * as admin from 'firebase-admin';

export const ITEM_COLLECTION_NAME = 'shopping-list';

export class ItemRepository {
  constructor(private _firestore: admin.firestore.Firestore = admin.firestore()) { }

  create(newDoc: Item = null): Promise<admin.firestore.DocumentReference>{
    return this._firestore.collection(ITEM_COLLECTION_NAME).add(newDoc);
  }
}
  • Go back to the src/items/item.service.ts file, immplement the remaining TODO comments by replacing them with calls to the created service method:
// Filename: src/items/item.service.ts

import * as admin from 'firebase-admin';

import { ItemRepository } from './item.repository';
import { Response } from '../shared/response';
import { Item } from './item.model';

export class ItemService {
  async addItem(newItem: Item = null): Promise<Response>{
    // NOTE: Should add more detailed validations
    if (newItem === null) { 
      return null;
    }

    const repository: ItemRepository = new ItemRepository();
    const writeResult: admin.firestore.DocumentReference = await repository.create(newItem);      
      
    return { result: `Item with ID: ${writeResult.id} added.` };
  }
}
  • At this point, we have completed all required code which does inserting a new JSON document into Firestore. As we have did in src/shared folder, it would be nice if we create src/items/index.ts file as well, and export all types within src/items just to make all import statements on any affected code files become more shorter and neat.
// Filename: src/items/index.ts

export * from './item.model';
export * from './item.repository';
export * from './item.service';
export * from './item.routes';
// Filename: src/items/item.service.ts

import * as admin from 'firebase-admin';

import { Item, ItemRepository } from '.';
import { Response } from '../shared';

export class ItemService {
  async addItem(newItem: Item = null): Promise<Response>{
    // NOTE: Should add more detailed validations
    if (newItem === null) { 
      return null;
    }

    const repository: ItemRepository = new ItemRepository();
    const writeResult: admin.firestore.DocumentReference = await repository.create(newItem);      
      
    return { result: `Item with ID: ${writeResult.id} added.` };
  }
}
// Filename: src/items/item.repository.ts

import * as admin from 'firebase-admin';

import { Item } from '.';

export const ITEM_COLLECTION_NAME = 'shopping-list';

export class ItemRepository {
  constructor(private _firestore: admin.firestore.Firestore = admin.firestore()) { }

  create(newDoc: Item = null): Promise<admin.firestore.DocumentReference>{
    return this._firestore.collection(ITEM_COLLECTION_NAME).add(newDoc);
  }
}
// Filename: src/items/item.routes.ts

import * as express from 'express';

import { ItemService } from '.';
import { Response } from '../shared';

export const ItemsRouter = express.Router();
ItemsRouter.post("/", async (req, res) => {
  let response: Response = null;
  try{
    // Grab the request's body
    console.log(`[DEBUG] - <items.routes.addItem> req.body: \n`, req.body);
    const itemService: ItemService = new ItemService();
    response = await itemService.addItem(req.body);
    console.log(`[DEBUG] - <items.routes.addItem> response: \n`, response);
    res.status(201).json(response);
  } catch(error) {
    console.log('[ERROR] - <items.routes.addItem>. Details: \n', error);
    response.error = {
      code: "400",
      message: "Creating a new Item is failing."
    };
    res.sendStatus(500).json(response);
  }
});

Implement remaining TODO comments in the main entry code

At this point, we should have all required components being implemented including the API Router component. In the main entry code, the src/index.ts file, we add code which import the Items router component and register it into the Express app object as shown in this following code:

// Filename: src/index.ts

import * as express from 'express';
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';

import { ItemsRouter } from './items';

// Initialise Firebase app & suppress a default warning when accessing firestore later.
admin.initializeApp();    
const firestore: admin.firestore.Firestore = admin.firestore();
firestore.settings({ timestampsInSnapshots: true });

// Create express application 
const app = express();

// TODO: Initialise required express middlewares

// TODO: Add more required routes
app.use(`/items`, ItemsRouter);

// Expose the Express API Routes as functions
export const api = functions.https.onRequest(app);

Then, we'll deploy current project into Firebase which will be explained in next section.

As for other APIs which do PUT, DELETE and GET, we'll let you to create them as coding exercise activities for you.

Build & Deploy the Firebase functions

  • Back to the console terminal and change current directory as the project's root directory, then run firebase deploy --only functions command, to deploy the API Functions into Firebase. Confirm that the deployment process is finished with no errors.

Testing the deployed API

  • Go back to your Firebase web console. Notice that the addItem function is displayed in the Functions main section. Note the function's url (e.g. https://us-central1-wendysa-firebase-demo.cloudfunctions.net/api). Recall the Router's path name we passed in (e.g. '/items'), inside the main entry code (src/app.ts). Based on this router's path name, we can obtain the actual addItem API URL as https://us-central1-wendysa-firebase-demo.cloudfunctions.net/api/items.

  • Install and run Postman. Use the Postman application to call the API. Confirm that a new item is created on the firestore database.

Securing the API Functions

The API endpoints that we have created earlier are accessible to anyone without any restrictions. Mostly, this is not desired behaviour. We need to restrict user's access on them. In this section, we are going to covers ways of how to restrict access to the API Functions through using Firebase Authentication feature.

Setup Firebase Authentication's Sign-In Method

  • Go to your Firebase web console page and then click Authentication menu.

  • On the Authentication page, click Sign-in method tab.

  • On the Sign-in providers list, pick one of providers that you desire to use. In this example, we'll going to use Google as the Sign-in provider. Therefore, click the Goggle item.

  • On the expanded Google's item, click Enable switch button, fill in Project support email field then click Save button.

Create a simple Angular Web for Sign-In the user and display the Token bearer string.

In the prior sub section, we have enabled Google Sign in method. This mean, anyone who want to access our API, need to have Google Account and sign in into your "system" in order to access our API. The only way to provide Sign In by using Google Account is by creating a web login page to handle this. Below are steps of how to develop this simple login page.

  • Ensure that you have installed ng-cli command line tool. Run npm i -g @angular/cli command for installing it.

  • Create a new angular application through running ng new <project-name> command. Example: ng new sign-in-firebase-demo. Confirm that project creation is finished successfully.

  • Change directory into your Firebase project's root directory, then run firebase init command.

TODO: Add more steps

Add authentication middleware on the Firebase Function's main entry code.

In this part, we are going to write a custom middleware

TODO: Add more steps

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment