-
Ensure that you have installed
Node.js
in your machine. -
Install
firebase tools
CLI through runningnpm i -g firebase-tools
command.
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 --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 withFirebase Functions
dependencies and initial code files. -
From the project's directory, change current directory's location into
functions
sub directory and runnpm i
to installfirebse-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 clickingAdd 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 clickCreate 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 yourFirebase
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 theFirebase
project's root directory.
-
On the console terminal, change current directory's location to the Project's
functions
directory, then installexpress
library:npm i express --save
. -
Rename or delete current
functions/src/index.ts
file and create a newindex.ts
infunctions/src
directory. -
By using code editor such as
vim
orvisual studio code
, open the newindex.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);
-
Back to your browser, open your
Firebase
project, on the Firebase web console. On theFirebase
web console page, clickDatabase
menu, located on left-hand side navigation menu, underDevelop
section. -
On the page's main section ("Cloud Firestore"), click
Create Database
button. Confirm that theSecurity rules for Cloud Firestore
dialog appears. -
On the
Security rules for Cloud Firestore
dialog, selectStart in test mode
option and then clickEnable
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.
-
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 asitems
orproducts
. 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 ofItems
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.
-
1st, we are going to create a new folder under
src
, name it asshared
. -
Inside the
shared
folder, create a newTypescript
file 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.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 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` };
}
}
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 insrc/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 asItem
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` };
}
}
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 thesrc/items
folder 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.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 createsrc/items/index.ts
file as well, and export all types withinsrc/items
just to make allimport
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);
}
});
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 functions
command, to deploy the API Functions intoFirebase
. Confirm that the deployment process is finished with no errors.
-
Go back to your
Firebase
web console. Notice that theaddItem
function is displayed in theFunctions
main 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 actualaddItem
API URL ashttps://us-central1-wendysa-firebase-demo.cloudfunctions.net/api/items
. -
Install and run
Postman
. Use thePostman
application 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
Firebase
web console page and then clickAuthentication
menu. -
On the
Authentication
page, clickSign-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 useGoogle
as the Sign-in provider. Therefore, click the Goggle item. -
On the expanded Google's item, click
Enable
switch button, fill inProject support email
field then clickSave
button.
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. Runnpm 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 runfirebase init
command.
TODO: Add more steps
In this part, we are going to write a custom middleware
TODO: Add more steps