-
Ensure that you have installed
Node.jsin your machine. -
Install
firebase toolsCLI through runningnpm i -g firebase-toolscommand.
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.
-
Login into your firebase account through invoking
firebase login --interactivecommand. -
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 functionscommand to initialise the project withFirebase Functionsdependencies and initial code files. -
From the project's directory, change current directory's location into
functionssub directory and runnpm ito installfirebse-admin&firebase-functionsnpm libraries. -
Go back to your web browser, browse to
Fireabse Consoleweb page, do login by using your Gmail account then create a new project through clickingAdd projectbutton 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 clickCreate Projectbutton. -
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 --addcommand. Confirm that command prompt show and suggest you with a list of available projects in yourFirebaseaccount. -
On the available
Firebaseprojects 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.firebasercthat is created in theFirebaseproject's root directory.
-
On the console terminal, change current directory's location to the Project's
functionsdirectory, then installexpresslibrary:npm i express --save. -
Rename or delete current
functions/src/index.tsfile and create a newindex.tsinfunctions/srcdirectory. -
By using code editor such as
vimorvisual studio code, open the newindex.tsfile 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);
-
Back to your browser, open your
Firebaseproject, on the Firebase web console. On theFirebaseweb console page, clickDatabasemenu, located on left-hand side navigation menu, underDevelopsection. -
On the page's main section ("Cloud Firestore"), click
Create Databasebutton. Confirm that theSecurity rules for Cloud Firestoredialog appears. -
On the
Security rules for Cloud Firestoredialog, selectStart in test modeoption and then clickEnablebutton. 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.
-
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/functionsdirectory, name is asitemsorproducts. 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
Typescriptwith appropriate name (e.g.item.routes.ts). Inside the routes file, we define the Route ofItemsPOST 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.
-
1st, we are going to create a new folder under
src, name it asshared. -
Inside the
sharedfolder, create a newTypescriptfile and name it asresponse.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.tsfile 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 a new
Typescriptfile 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` };
}
}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
Typescriptfile insrc/itemsfolder 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.tsfile, and mark any item arguments to be typed asIteminterface:
// 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` };
}
}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
Typescriptfile in thesrc/itemsfolder and give it an appropriate name such asitem.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.tsfile, 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/sharedfolder, it would be nice if we createsrc/items/index.tsfile as well, and export all types withinsrc/itemsjust to make allimportstatements 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);
}
});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.
- Back to the console terminal and change current directory as the project's root directory, then run
firebase deploy --only functionscommand, to deploy the API Functions intoFirebase. Confirm that the deployment process is finished with no errors.
-
Go back to your
Firebaseweb console. Notice that theaddItemfunction is displayed in theFunctionsmain section. Note the function'surl(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 actualaddItemAPI URL ashttps://us-central1-wendysa-firebase-demo.cloudfunctions.net/api/items. -
Install and run
Postman. Use thePostmanapplication to call the API. Confirm that a new item is created on the firestore database.
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.
-
Go to your
Firebaseweb console page and then clickAuthenticationmenu. -
On the
Authenticationpage, clickSign-in methodtab. -
On the
Sign-in providerslist, pick one of providers that you desire to use. In this example, we'll going to useGoogleas the Sign-in provider. Therefore, click the Goggle item. -
On the expanded Google's item, click
Enableswitch button, fill inProject support emailfield then clickSavebutton.
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-clicommand line tool. Runnpm i -g @angular/clicommand 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
Firebaseproject's root directory, then runfirebase initcommand.
TODO: Add more steps
In this part, we are going to write a custom middleware
TODO: Add more steps