The Angular Firebase Survival Guide 465103162
The Angular Firebase Survival Guide 465103162
Jeff Delaney
This book is for sale at http://leanpub.com/angularfirebase
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and
many iterations to get reader feedback, pivot until you have the right book and build traction once
you do.
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Why Angular? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Why Firebase? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Why Angular and Firebase Together? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
This Book is for Developers Who… . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Angular Firebase Starter App . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Watch the Videos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Join the Angular Firebase Slack Team . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
The Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.1 Top Ten Best Practices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2 Start a New App from Scratch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Separating Development and Production Environments . . . . . . . . . . . . . . . . . 7
1.4 Importing Firebase Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.5 Deployment to Firebase Hosting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Cloud Firestore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.0 Cloud Firestore versus Realtime Database . . . . . . . . . . . . . . . . . . . . . . . . 11
2.1 Data Structuring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2 Collection Retrieval . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.3 Document Retrieval . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.4 Include Document Ids with a Collection . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.5 Add a Document to Collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.6 Set, Update, and Delete a Document . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.7 Create References between Documents . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.8 Set a Consistent Timestamp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.9 Use the GeoPoint Datatype . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.10 Atomic Writes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.11 Order Collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.12 Limit and Offset Collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.13 Querying Collections with Where . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.14 Creating Indices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.15 Backend Firestore Security Rules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
CONTENTS
Realtime Database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.0 Migrating from AngularFire Version 4 to Version 5 . . . . . . . . . . . . . . . . . . . 32
3.1 Data Modeling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.2 Database Retrieval as an Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.3 Show Object Data in HTML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.4 Subscribe without the Async Pipe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.5 Map Object Observables to New Values . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.6 Create, Update, Delete a FirebaseObjectObservable data . . . . . . . . . . . . . . . . 40
3.7 Database Retrieval as a Collection . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.8 Viewing List Data in the Component HTML . . . . . . . . . . . . . . . . . . . . . . . 42
3.9 Limiting Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.10 Filter Lists by Value . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.11 Create, Update, Delete Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.12 Catch Errors with Firebase Operations . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.13 Atomic Database Writes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
3.14 Backend Database Rules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
3.15 Backend Data Validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
User Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.1 Getting Current User Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.2 OAuth Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
4.3 Anonymous Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.4 Email Password Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.5 Handle Password Reset . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.6 Catch Errors during Login . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.7 Log Users Out . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.8 Save Auth Data to the Realtime Database . . . . . . . . . . . . . . . . . . . . . . . . 57
4.9 Creating a User Profile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4.10 Auth Guards to Protect Routes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
Why Angular?
Angular can produce maintainable cross-platform JavaScript apps that deliver an awesome user
experience. It’s open source, backed by Google, has excellent developer tooling via TypeScript, a
large community of developers, and is being adopted by large enterprises. I see more and more
Angular2+ job openings every week.
Introduction 2
Why Firebase?
Firebase eliminates the need for managed servers, scales automatically, dramatically reduces
development time, is built on Google Cloud Platform, and is free for small apps.
Firebase is a Backend-as-a-Service (BaaS) that also offers Functions-as-a-Service (FaaS). The Firebase
backend will handle your database, file storage, and authentication – features that would normally
take weeks to develop from scratch. Cloud functions will run background tasks and microservices
in a completely isolated NodeJS environment. On top of that, Firebase provides hosting with free
SSL, analytics, and cloud messaging.
Furthermore, Firebase is evolving with the latest trends in web development. In March 2017, the
platform introduced Cloud Functions for Firebase. Then in October 2017, the platform introduced
the Firestore Document Database. I have been blown away at the sheer pace and quality of new
feature roll-outs for the platform. Needless to say, I stay very busy keeping this book updated.
• Angular v4.4
• Angular CLI v1.4.3
• TypeScript v2.3
• AngularFire2 v5.0
Problem
You want a few guidelines and best practices for building Angular apps with Firebase.
Solution
Painless development is grounded in a few core principles. Here are my personal top ten ten tips for
Angular Firebase development.
Problem
You want start a new Angular project, using Firebase for the backend.
Solution
Let’s start with the bare essentials. (You may need to prefix commands with sudo).
The Basics 5
Setting up an Angular app with Firebase is easy. We are going to build the app with the Angular
CLI, specifying the routing module and SCSS for styling. Let’s name the app fire.
In the environments/environment.ts, add your credentials. Make sure to keep this file private by
adding it to .gitignore. You don’t want it exposed in a public git repo.
The Basics 6
That’s it. You now have a skeleton app ready for development.
The Basics 7
1 ng serve
Problem
You want maintain separate backend environments for develop and production.
Solution
It’s a good practice to perform development on an isolated backend. You don’t want to accidentally
pollute or delete your user data while experimenting with a new feature.
The first step is to create a second Firebase project. You should have two projects named something
like MyAppDevelopment and MyAppProduction.
Next, grab the API credentials and update the environment.prod.ts file.
The Basics 8
Now, in your app.module.ts, your app will use different backend variables based on the environ-
ment.
Test it by running ng serve for development and ng serve --prod for production.
Problem
You want to import the AngularFire2 or the Firebase SDK into a service or component.
Solution
Take advantage of tree shaking with AngularFire2 to only import the modules you need. In many
cases, you will only need the database or authentication, but not both. Here’s how to import them
into a service or component.
The Basics 9
You can also import the firebase SDK directly when you need functionality not offered by
AngularFire2. Firebase is not a NgModule, so no need to include it in the constructor.
Problem
You want to deploy your production app to Firebase Hosting.
Solution
It is a good practice to build your production app frequently. It is common to find bugs and
compilation errors when specifically when running an Ahead-of-Time (AOT) build in Angular.
During development, Angular is running with Just-In-Time (JIT) compilation, which is more
forgiving with type safety errors.
1 ng build --prod
1 firebase init
1. Choose hosting.
2. Change public folder to dist when asked (it defaults to public).
3. Configure as single page app? Yes.
4. Overwrite your index.html file? No.
1 firebase deploy
If all went well, your app should be live on the firebase project URL.
Cloud Firestore
Firestore was introduced into the Firebase platform on October 3rd, 2017. It is a superior alternative
(in most situations) to the Realtime Database that is covered in Chapter 3.
What is Firestore?
Firestore is a NoSQL document-oriented database, similar to MongoDB, CouchDB, and AWS
DynamoDB.
It works by storing JSON-like data into documents, then organizes them into collections that can
be queried. All data is contained on the document, while a collection just serves as a container.
Documents can contain their own nested subcollections of documents, leading to a hierarchical
structure. The end result is a database that can model complex relationships and make multi-property
compound queries.
Unlike a table in a SQL database, a Firestore document does not adhere to a data schema. In other
words, document-ABC can look completely different from document-XYZ in the same collection.
However, it is a good practice to keep data structures as consistent as possible across collections.
Firestore automatically indexes documents by their properties, so your ability to query a collection
is optimized by a consistent document structure.
The goal of this chapter is to introduce data modeling best practices and teach you how perform
common tasks with Firestore in Angular.
Problem
You’re not sure if you should use Firestore or the Realtime Database.
Cloud Firestore 12
Solution
I follow a simple rule - use Firestore, unless you have a good reason not to.
However, if you can answer TRUE to ALL statements below, the Realtime Database might worth
exploring.
Problem
You want to know how to structure your data in Firestore.
Cloud Firestore 13
Solution
You already know JavaScript, so think of a collection as an Array and a document as an Object.
What’s Inside a Document?
A document contains JSON-like data that includes all of the expected primitive datatypes like strings,
numbers, dates, booleans, and null - as well as objects and arrays.
Documents also have several custom datatypes. A GeoPoint will automatically validate latitude and
longitude coordinates, while a DocumentReference can point to another document in your database.
We will see these special datatypes in action later in the chapter.
Best Practices
Firestore pushes you to form a hierarchy of data relationships. You start with (1) a collection in the
root of the database, then (2) add a document inside of it, then (3) add another collection inside that
document, then (4) repeat steps 2 and 3 as many times as you need.
1. Always think about HOW the data will be queried. Your goal is to make data retrieval fast
and efficient.
2. Collections can be large, but documents should be small.
3. If a document becomes too large, consider nesting data in a deeper collection.
In this example, we have a collection of posts with some basic content data, but posts can also receive
comments from users. We could save new comments directly on the document, but would that scale
well if we had 10,000 comments? No, the memory in the app would blow up trying to retrieve this
data. In fact, Firestore will throw an error for violating the 1 Mb document size limit well before
Cloud Firestore 14
reaching this point. A better approach is to nest a comments subcollection under each document
and query it separately from the post data. Document retrieval is shallow - only the top level data
is returned, while nested collections can be retrieved separately.
1 ++postsCollection
2 postDoc
3 - author
4 - title
5 - content
6 ++commentsCollection
7 commentDocFoo
8 - text
9 commentDocBar
10 - text
For group chat, we can use two root level collections called users and chats. The user document is
simple - just a place to keep basic user data like email, username, etc.
A chat document stores basic data about a chat room, such as the participating users. Each room
has a nested collection of messages (just like the previous example). However, the message makes
a reference to the associated user document, allowing us to query additional data about the user if
we so choose.
A document reference is very similar to a foreign key in a SQL database. It is just a pointer to a
document that exists at some other location in the database.
Cloud Firestore 15
1 ++usersCollection
2 userDoc
3 - username
4 - email
5
6 ++chatsCollection
7 chatDoc
8 - users[]
9 ++messagesCollection
10 messageDocFoo
11 - text
12 - userDocReference
13 messageDocBar
14 - userDocReference
In the graphic above, we can see how the movies collection and users collection have a two-way
connection through the middle-man stars collection. All data about a relationship is kept in the star
document - data never needs to change on the connected user/movie documents directly.
Having a root collection structure allows us to query both “Movie reviews” and “User reviews”
independently. This would not be possible if stars were nested as a sub collection. This is similar to
a many-to-many-through relationship in a SQL database.
Cloud Firestore 16
1 ++usersCollection
2 userDoc
3 - username
4 - email
5
6 ++starsCollection
7 starDoc
8 - userId
9 - movieId
10 - value
11
12 ++moviesCollection
13 movieDoc
14 - title
15 - plot
Problem
You want to retrieve a collection of documents.
Solution
A collection of documents in Firestore is like a table of rows in a SQL database, or a list of objects
in the Realtime Database. When we retrieve a collection in Angular, the endgame is to generate an
Observable array of objects [{...data}, {...data}, {...data}] that we can show the end user.
The examples in this chapter will use the TypeScript Book interface below. AngularFire requires a
type to be specified, but you can opt out with the any type, for example AngularFirestoreCollec-
tion<any>.
I am setting up the code in an Angular component, but you can also extract this logic into a service
to make it available (injectable) to multiple components.
Reading data in AngularFire is accomplished by (1) making a reference to its location in Firestore,
(2) requesting an Observable with valueChanges(), and (3) subscribing to the Observable.
Steps 1 and 2: book-info.component.ts
Step 3: book-info.component.html
The ideal way to handle an Observable subscription is with the async pipe in the HTML. Angular
will subscribe (and unsubscribe) automatically, making your code concise and maintainable.
Problem
You want to retrieve a single document.
Solution
Every document is created with a auto-generated unique ID. If you know the unique ID, you can re-
trieve the document with the same basic process as a collection, but using the afs.doc('collection/docId')
method.
book-info.component.html
When working with an individual document, it is useful to set the unwrapped Observable as a
template variable in Angular. This little trick allows you to use the async pipe once, then call any
property on the object - much cleaner than an async pipe on every property.
Cloud Firestore 20
Problem
You want the document IDs included with a collection.
Solution
By default, valueChanges() does not map the document ID to the document objects in the array.
In many cases, you will need the document ID to make queries for individual documents. We can
satisfy this requirement by pulling the entire snapshot from Firestore and mapping it’s metadata to
a new object.
This is not the most beautiful code in the world, but it’s the best we can do at this point. If you
perform this operation frequently, I recommend building a generic Angular service that can apply
the code to any collection.
Problem
You want to add a new document to a collection.
Cloud Firestore 21
Solution
Collections have an add() method that takes a plain JavaScript object and creates a new document
in the collection. The method will return a Promise that resolves when the operation is successful,
giving you the option to execute additional code after the operation succeeds or fails.
Problem
You want to set, update, and delete individual documents.
Solution
Write operations are easy to perform in Firestore. You have the following three methods at your
disposal.
• set() will destroy all existing data and replace it with new data.
• update() will only modify existing properties.
• delete() will destroy the document.
Cloud Firestore 22
All operations return a Promise that resolves when the operation is successful, giving you the option
to execute additional code after the operation succeeds or fails.
1 doc.update(data)
2 .then(() => console.log('success') )
3 .catch(err => console.log(err) )
Problem
You want to create a reference between two related documents.
Solution
Document references provide a convenient way to model data relationships, similar to the way
foreign keys work in a SQL database. We can set them by sending a DocumentReference object to
firestore. In AngularFire, this is as simple as calling the ref property on the document reference.
Here’s how we can host a reference to a user document on a book document.
Problem
You want to maintain a consistent server timestamp on database records.
Cloud Firestore 23
Solution
Setting timestamps with the JavaScript Date class does not provide consistent results on the server.
Fortunately, we can tell Firestore to set a server timestamp when running write operations.
I recommend setting up a TypeScript getter to make the timestamp call less verbose. Simply pass the
object returned from FieldValue.serverTimestamp() as the value to any property that requires a
timestamp.
Problem
You want to save geolocation data in Firestore.
Solution
We need to send geolocation data to Firestore as an instance of the GeoPoint class. I recommend
setting up a helper method to return the instance from the Firebase SDK. From there, you can use
the GeoPoint as the value to any property that requires latitude/longitude coordinates.
Problem
You want to perform multiple database writes in a batch that will succeed or fail together.
Cloud Firestore 24
Solution
Using the firebase SDK directly, we can create batch writes that will update multiple documents
simultaneously. If any single operation fails, none of the changes will be applied. It works setting
all operations on the batch instance, then runs them with batch.commit(). If any operation in the
batch fails, the database rolls back to the previous state.
Problem
You want a collection ordered by a specific document property.
Solution
Let’s assume we have the following documents in the books collection.
Keep in mind, Firestore does not order by ID, so it is important to set documents with an property
that makes sense for ordering, such as a timestamp.
Ordering is not just limited to numeric values - we can also order documents alphabetically.
Problem
You want a specific number of documents returned in a collection.
Solution
As your collections grow larger, you will need to limit collections to a manageable size.
For the sake of this example, let’s assume we have millions of books in our collection.
The limit() method will return the first N documents from the collection. In general, it will always
be used in conjunction with orderBy() because documents have no order by default.
When it comes to offsetting data, you have four methods at your disposal. I find it easier write them
out in a sentence.
If we want all books written after a certain year, we run the query like so:
Cloud Firestore 26
If we change it to startAfter(), books from 1969 will be excluded from the query.
These methods are very useful when it comes to building pagination and infinite scroll features in
apps.
Problem
You want query documents with equality and/or range operators.
Solution
The where() method provides an expressive way to filter data in a collection. The beauty of the
method is that it works just like it reads. It requires three arguments ref.where(field, operator,
value).
Let’s look at some examples and read them like sentences. First, we can filter by equality.
But there is one major exception! You cannot combine range operators on multiple properties.
Problem
You want to order a collection by multiple properties, which requires a custom index.
Solution
Firestore will automatically create an index for every individual property in your collection.
However, it would result in an enormous amount of indices if Firestore indexed every combination
of properties. A document with just 7 properties has 120 possible index combinations if you follow
the rule of Eularian numbers.
The best way to create an index is to simply wait for Firestore to tell you when one is necessary. If
we try to order by two different properties, we should get an error in the browser console.
Cloud Firestore 28
The error message will provide a URL link to the Firestore console to create the index. Create the
index, and the error will have disappeared the next time you run the query.
Problem
You want to secure your backend data to authorized users with firestore security rules.
Solution
All Firestore rules must evaluate to a boolean value (true or false). Writing a rule is like saying “If
some condition is true, I grant you permission to read/write this data”.
There are thousands of possible security rule scenarios and I can’t cover them all, but I can show
you what I consider the most useful configurations.
1 match /itemsCollection/itemXYZ
1 match /itemsCollection/{itemID}
1 match /itemsCollection/{itemID=**}
1 service cloud.firestore {
2 match /databases/{database}/documents {
3 match /{document=**} {
4 allow read;
5 allow write;
6 }
7 }
8 }
Note: From here on out, I am going to omit the code surrounding the database to avoid repeating
myself.
Full Security: Nobody can read or write
If you need to lock down your database completely, add this rule.
1 match /{document=**} {
2 allow read: if false;
3 allow write: if false;
4 }
1 match /{document=**} {
2 allow read, write: if request.auth != null;
3 }
1 match /users/{userId} {
2 allow read, write: if request.auth.uid == userID;
3 }
1 function isModerator(userId) {
2 get(/databases/$(database)/documents/users/$(userId)).data.isModerator =\
3 = true;
4 }
5
6 match /forum/{postID} {
7 allow delete: if isModerator(request.auth.uid);
8 }
Regex Security
You can perform a a regular expression match to ensure data adheres to a certain format. For
example, this rule will only allow writes of the email address ends in @angularfirebase.com
1 match /{document} {
2 allow write: if document.matches('.*@angularfirebase\.com')
3 }
Time Security
You can also get the exact timestamp in UTC format from the request to compare to an existing
timestamp in the database.
Cloud Firestore 31
1 match /{document} {
2 allow write: if request.time < resource.data.timestamp + duration.value(\
3 1, 'm');
4 }
Realtime Database
Firebase provides a realtime NoSQL database. This means all clients subscribe to one database
instance and listen for changes. As a developer, it allows you to handle database as an asynchronous
data stream. Firebase has abstracted away the pub/sub process you would normally need to build
from scratch using something like Redis.
Here are the main things you should know when designing an app with Firebase.
Problem
You want to migrate an existing app from AngularFire <= v4 to v5. (If you’re brand new to
AngularFire, skip this snippet).
Realtime Database 33
Solution
AngularFire v5.0.0 was released in October 2017 and was a complete rewrite of the realtime database
API. It introduced significant breaking changes to previous versions, so I want to provide a quick
migration guide for developers in the middle of existing projects.
Quick Fix
After you upgrade to v5, your database code will break catastrophically. Fortunately, the AngularFire
core team realized this issue and kept the old API available under a different namespace of database-
deprecated. You can make your code work by simply updating your imports.
Do a project search for “angularfire2/database” and replace all instances with “angularfire2/database-
deprecated”.
You code should now look like this:
1 import {
2 AngularFireDatabase,
3 FirebaseObjectObservable,
4 FirebaseListObservable
5 } from 'angularfire2/database-deprecated';
Full Fix
Fully migrating to the new API is going to be a little more tedious. The main difference in v5 is the
decoupling of the Observable from its reference to firebase.
Let’s compare the APIs.
Here is the basic process you will need to follow to update from v4 to v5:
1. For database write operations (push, update, set, remove), you will need to convert every
Firebase(List | Object)Observable into the new AngularFire(List | Object) reference.
2. To read data as an Observable you will need to call valueChanges() or snapshotChanges()
on the reference created in the previous step.
Problem
You want to know how to model data for Firebase NoSQL.
Solution
In NoSQL, you should always ask “How am I going to be querying this data?”, because operations
must be executed quickly. Usually, that means designing a database that is shallow or that avoids
large nested documents. You might even need to duplicate data and that’s OK - I realize that might
freak you out if you come from a SQL background. Consider this fat and wide design:
1 -|users
2 -|userID
3 -|books
4 -|bookID
5 -|comments
6 -|commentID
7 -|likes
Now imagine you wanted to loop over the users just to display their usernames. You would also
need load their books, the book comments, and the likes – all that data just for some usernames. We
can do better with a tall and skinny design - a denormalized design.
Realtime Database 35
1 -|users
2 -|userID
3
4 -|books
5 -|userId
6 -|bookID
7
8 -|comments
9 -|bookID
10
11 -|likes
12 -|commentID
Problem
You want to retrieve and subscribe to data from Firebase as a single object.
Solution
You should retrieve data as an object when you do not plan iterating over it. For example, let’s
imagine we have a single book in our database.
The AngularFireObject<T> requires a TypeScript type to be specified. If you want to opt out, you
can use AngularFireObject<any>, but it’s a good idea to statically type your own interfaces:
AngularFireList<>
Problem
You want to show the Observable data in the component HTML template.
Solution
We have a Observable<Book>. How do we actually get data from it? The answer is we subscribe to
it. Angular has a built async pipe2 that will subscribe (and unsubscribe) to the Observable from the
template.
1 <article>
2 {{ bookObservable | async | json }}
3
4 {{ (bookObservable | async)?.content }}
5 </article>
We unwrap the Observable in parenthesis before trying to call its attributes. Calling bookObserv-
able.author would not work because that attribute does not exist on the Observable itself, but
rather its emitted value. The result should look like this:
2
https://angular.io/api/common/AsyncPipe
Realtime Database 38
If you have an object with many properties, consider setting the unwrapped Observable as a template
variable in Angular. This little trick allows you to use the async pipe once, then call any property
on the object - much cleaner than an async pipe on every property.
Problem
You want to extract Observable data in the component TypeScript before it reaches the template.
Solution
Sometimes you need to play with the data before it reaches to the template. We can replicate the
async pipe in the component’s TypeScript, but it takes some extra code because we must create the
subscription, then unsubscribe when the component is destroyed to avoid memory leaks.
Realtime Database 39
1 //// book-info.component.ts
2 import { Subscription } from 'rxjs/Subscription';
3
4 subscription: Subscription;
5 bookRef: AngularFireList<Book>;
6 bookData: Book;
7
8 ngOnInit() {
9 this.bookRef = this.db.object('books/atlas-shrugged');
10
11 this.subscription = this.bookRef.valueChanges()
12 .subscribe(book => {
13 this.bookData = book
14 })
15 }
16
17 ngOnDestroy() {
18 this.subscription.unsubscribe()
19 }
In the HTML, the async pipe is no longer needed because we unwrapped the raw data in the
TypeScript with subscribe.
1 {{ bookData | json }}
2
3 {{ bookData?.content }}
Problem
Problem you want to alter Observable values before they are emitted in a subscription.
Realtime Database 40
Solution
RxJS ships with all sorts to helpful operators to change the behavior of Observables. For now, I will
demonstrate map because it is the most frequently used in Angular.
Let’s get the object Observable, then map its author property to an uppercase string.
1 this.bookObserbable = this.bookRef
2 .map(book => book.author.toUpperCase() )
1 {{ bookObservable | async }}
But the result will be a string of AYN RAND, instead of the JS object displayed in section 3.3.
Problem
You know how to retrieve data, but now you want to perform operations on it.
Solution
You have three available operators to manipulate objects.
1. Set - Destructive update. Deletes all data, replacing it with new data.
2. Update - Only updates specified properties, leaving others unchanged.
3. Remove - Deletes all data.
Here are three methods showing you how to perform these operations on an AngularfireObject.
Realtime Database 41
1 createBook() {
2 const book = { title: 'War and Peace' }
3 return this.db.object('/books/war-and-peace')
4 .set(book)
5 }
6
7 updateBook(newTitle) {
8 const book = { title: newTitle }
9 return this.db.object('/books/war-and-peace')
10 .update(book)
11 }
12
13 deleteBook() {
14 return this.db.object('/books/twilight-new-moon')
15 .remove()
16 }
Problem
You want to retrieve data from Firebase as a list or array.
Solution
The AngularFireList is ideal when you plan on iterating over objects, such as a collection of books.
The process is exactly the same as an object, but we expect an Array of objects.
Realtime Database 42
1 //// books-list.component.ts
2
3 booksRef: AngularFireList<Book>;
4 booksObservable: Observable<Book[]>; // <-- notice the [] here
5
6 ngOnInit() {
7 // Step 1: Make a reference
8 this.booksRef = this.db.list('books');
9
10 // Step 2: Get an observable of the data
11 this.bookObservable = this.booksRef.valueChanges();
12 }
Problem
You want to iterate over an Observable list in the HTML template.
Realtime Database 43
Solution
Again, you should take advantage of Angular’s async pipe to unwrap the Observable in the template.
This will handle the subscribe and unsubscribe process automagically.
1 <ul>
2 <li *ngFor="let book of booksObservable | async">
3 {{ book.title }} by {{ book.author }}
4 </li>
5 </ul>
Problem
You want to limit the number of results in a collection.
Solution
You can pass a second callback argument to db.list(path, queryFn) to access Firebase realtime
database query methods. In this example, we limit the results to the first 10 books in the database.
Realtime Database 44
1 queryBooks() {
2 return this.db.list('/books' ref => ref.limitToFirst(10) )
3 }
Problem
You want to return list items that have a specific property value.
Solution
This time, let’s filter the collection to all books with an author property of Jack London.
1 queryBooks() {
2 return this.db.list('/books', ref => {
3 return ref.orderByChild('author').equalTo('Jack London')
4 })
5 }
Problem
You want create, update, or remove values in a list Observable.
Solution
When creating new books, we push them to the list. This will create a push key automatically, which
is an encoded timestamp that looks like “-Xozdf2i23sfdf73”. You can think of this the unique ID for
an item in a list.
Update and delete operations are similar to objects, but require the key of the item as an argument.
The key is not returned with valueChanges(), so I included a helper method booksWithKeys that
will return an Observable array with the pushKeys included.
Realtime Database 45
Problem
You want to handle errors gracefully when a Firebase operation fails.
Solution
Data manipulation (set, update, push, remove) functions return a Promise, so we can determine
success or error by calling then and/or catch. In this example, a separate error handler is defined
that can be reused as needed. You might want to add some logging, analytics, or messaging logic to
the handleError function.
Realtime Database 46
1 this.createBook()
2 .then( () => console.log('book added successfully'))
3 .catch(err => handleError(err) );
4
5 this.updateBook()
6 .then( () => console.log('book updated!'))
7 .catch(err => handleError(err) );
8
9 private handleError(err) {
10 console.log("Something went horribly wrong...", err)
11 }
Problem
You want to update multiple database locations atomically, to prevent data anomalies.
Solution
You will often find situations where you need to keep multiple collections or documents in sync
during a single operation. In database theory, this is known as an atomic operation. For example,
when a user comments on a book, you want to update the user’s comment collection as well as the
book’s comment collection simultaneously. If one operation succeeded, but the other failed, it would
lead to a data mismatch or anomaly.
In this basic example, we will update the tag attribute on two different books in a single operation.
But be careful - this example will perform at destructive set, even though it calls update.
1 atomicSet() {
2 let updates = {};
3 updates['books/atlas-shrugged/tags/epic'] = true;
4 updates['tags/epic/atlas-shrugged'] = true
5
6 this.db.object('/').update(updates)
7 }
Problem
You want to secure read/write access to your data on the backend.
Solution
Firebase allows you to define database security logic in JSON format that mirrors to the structure of
your database. You just write logical statements that evaluate to true or false, giving users access
to read or write data at a given location.
First, let’s go over a few special built-in variables you should know about.
auth – The current user’s auth state. root – The root of the database and can be traversed with
.child(‘name’). data – Data state before an operation (the old data) newData – Data after an operation
(the new data) now – Unix epoch timestamp ${wildcard} – Wildcard, used to compare keys.
Let’s start by locking down the database at the root. Nothing goes in, nothing comes out.
1 "rules": {
2 ".read": false,
3 ".write": false
4 }
1 "rules": {
2 ".read": "auth != null",
3 ".write": false
4 }
Now let’s allow users to write to the books collection, but only if the data is under their own UID.
Realtime Database 48
1 "rules": {
2 ".read": "auth != null",
3 "books": {
4 "$uid": {
5 ".write": "$uid === auth.uid"
6 }
7 }
8 }
Now, let’s assume we have moderator users, who have access to write to any user’s book. Notice the
use of the OR || operator in the rule to chain an extra condition. You can also use AND && when
multiple conditions must be met.
1 "rules": {
2 ".read": "auth != null",
3 "books": {
4 "$uid": {
5 ".write": "$uid === auth.uid
6 || root.child('moderators').child(auth.uid).val() === true"
7 }
8 }
9 }
Problem
You want to validate data before it’s written to the database.
Solution
Firebase has a third rule, .validate, which allows you to put constraints on the type of data that
can be saved on the backend. The incoming data will be in the newData Firebase variable.
1 "rules": {
2 "books": {
3 "$bookId": {
4 "title": {
5 ".validate": "newData.isString()"
6 }
7 }
8 }
9 }
10
11 You will likely want to chain multiple validations together.
12
13 ```json
14 {
15 ".validate": "newData.isString()
16 && newData.val().matches('regex-expression')"
17 }
You might have a list of allowed values in your database, let’s image categories. You can validate
against them by traversing the database.
1 {
2 ".validate": "root.child('categories/' + newData.val()).exists()"
3 }
When creating an object, you might want to validate it has all the required attributes.
1 {
2 "$bookId": {
3 ".validate": "newData.hasChildren(['title', 'body', 'author'])",
4 "title": {
5 ".validate": "newData.isString()"
6 },
7 "body": {},
8 "author": {}
9 }
10 }
User Authentication
Firebase provides a flexible authentication system that integrates nicely with Angular and RxJS. In
this chapter, I will show you how use three different paradigms, including:
Problem
You want to obtain the current user data from Firebase.
User Authentication 51
Solution
AngularFire2 returns an authState Observable that contains the important user information, such
as the UID, display name, email address, etc. You can obtain the current user as an Observable like
so.
Alternatively, you can unwrap the auth observable by by subscribing to it. This may be necessary
if you need the UID to load other data from the database
1 currentUser = null;
2
3 // or ngOnInit for components
4 constructor(afAuth: AngularFireAuth) {
5 afAuth.authState.subscribe(userData => {
6 this.currentUser = userData
7 });
8 }
At this point, the authState will be null. In the following sections, it will be populated with different
login methods.
User Authentication 52
OAuth Video
https://youtu.be/-3rkY8X2EWc
Problem
You want to authenticate users via Google, Facebook, Github, or Twitter.
Solution
Firebase makes OAuth a breeze. In the past, this was the most difficult form of authentication for
developers to implement. From the Firebase console, you need to manually activate the providers
you want to use. Google is ready to go without any configuration, but other providers like Facebook
or Github, require you to get your own developer API keys.
Here’s how to handle the login process in a service.
1 googleLogin() {
2 const provider = new firebase.auth.GoogleAuthProvider()
3 return this.socialSignIn(provider);
4 }
5
6 facebookLogin() {
7 const provider = new firebase.auth.FacebookAuthProvider()
8 return this.socialSignIn(provider);
9 }
10
11 private socialSignIn(provider) {
12 return this.afAuth.auth.signInWithPopup(provider)
13 }
Now you can create login buttons in your component HTML that trigger the login functions on the
click event and Firebase will handle the rest.
1 <button (click)="googleLogin()"></button>
2 <button (click)="facebookLogin()"></button>
User Authentication 53
Problem
You want lazily register users with anonymous authentication.
Solution
Anonymous auth simply means creating a user session without collecting credentials to re-
authenticate, such as an email address and password. This approach is beneficial when you want a
guest user to try out the app, then register later.
1 anonymousLogin() {
2 return this.afAuth.auth.signInAnonymously()
3 }
That was easy, but the trick is upgrading their account. Firebase supports account upgrading, but
it’s not supported by AngularFire2, so let’s tap into the Firebase SDK. You can link or upgrade any
account by calling linkWithPopup.
Problem
You want a user to sign up with their email and password.
Solution
Email/password auth is the most difficult to setup because we need to run some form validation
and generate different views for new user sign up and returning user sign in. Here’s how you might
handle the process in a component.
1 userForm: FormGroup;
2
3 constructor(private fb: FormBuilder, private afAuth: AngularFireAuth) {}
4
5 ngOnInit() {
6 this.userForm = this.fb.group({
7 'email': ['', [
8 Validators.required,
9 Validators.email
10 ]
11 ],
12 'password': ['', [
13 Validators.pattern('^(?=.*[0-9])(?=.*[a-zA-Z])([a-zA-Z0-9]+)$'),
14 Validators.minLength(6),
15 Validators.maxLength(25)
16 ]
17 ]
18 });
19 }
20
21 emailSignUp() {
22 let email = this.userForm.value['email']
23 let password = this.userForm.value['password']
User Authentication 55
Problem
You need a way for users to reset their password.
Solution
Firebase has a built-in flow for resetting passwords. It works by sending the user an email with a
tokenized link to update the password - you just need a way to trigger the process directly via the
Firebase SDK.
User Authentication 56
1 userEmail: string;
2
3 resetPassword() {
4 const fbAuth = firebase.auth();
5 fbAuth.sendPasswordResetEmail(userEmail)
6 }
Use ngModel in the HTML template to collect the user’s email address. Then send the reset password
email on the button click.
Problem
You want to catch errors when login fails.
Solution
The login process can fail3 for a variety of reasons, so let’s refactor the social sign in function from
section 4.2. It is a good idea to create an error handler, especially if you use multiple login methods.
1 private socialSignIn(provider) {
2 return this.afAuth.auth.signInWithPopup(provider)
3 .then(() => console.log('success') )
4 .catch(error => handleError(error) );
5 }
6
7 private handleError(error) {
8 console.log(error)
9 // alert user via toast message
10 }
Problem
You want to end a user session.
3
https://firebase.google.com/docs/reference/js/firebase.auth.Error
User Authentication 57
Solution
As you can imagine, logging out is a piece of cake. Calling signOut() will destroy the session and
reset the current authState to null.
1 logout() {
2 this.afAuth.auth.signOut();
3 }
Problem
You want to save a user’s auth information to the realtime database.
Solution
The Firebase login function returns a Promise. We can catch a successful response by calling then
and running some extra code to update the database. Let’s refactor the our sign in function from
section 4.2 to save the user’s email address to the realtime database after sign in.
A good database structure for this problem has data nested under each user’s UID.
1 -| users
2 -| $uid
3 email: string
4 moderator: boolean
5 birthday: number
In the component, we call the desired signin function, which returns a Promise. When resolved, the
Promise provides a credential object with the user data that can be saved to the database.
User Authentication 58
1 private socialSignIn(provider) {
2 return this.afAuth.auth.signInWithPopup(provider)
3 .then(credential => {
4 const user = credential.user
5 this.saveEmail(user)
6 })
7 }
8
9 private saveEmail(user) {
10 if (!user) { return; }
11
12 const path = `users/${user.uid}`;
13 const data = { email: user.email }
14
15 this.db.object(path).update(data)
16 }
Problem
You want to display user data in profile page.
Solution
The Firebase auth object has some useful information we can use to build a basic user profile,
especially when used with OAuth. This snippet is designed to show you the default properties
available.
Let’s assume we have subscribed to the currentUser from section 4.1. You can simply call its
properties in the template.
1 <aside>
2 <p>{{ currentUser?.displayName }}</p>
3 <img [src]="currentUser?.photoUrl || defaultAvatar">
4 </aside>
Here are the Firebase default properties you can use to build user profile data.
• uid
• displayName
User Authentication 59
• photoUrl
• email
• emailVerified
• phoneNumber
• isAnonymous
You can add additional custom user details to the realtime database using the technique described
in section 4.9.
Problem
You want to prevent unauthenticated users from navigating to certain pages.
Solution
Guards provide a way to lock down routes until its logic resolves to true. This may look complex
(most of it is boilerplate), but it’s actually very simple. We take the first emission from the AuthState
Observable, map it to a boolean, and if false, the user is redirected to a login page. You can generate
the guard with the CLI via ng generate guard auth;
19 .take(1)
20 .map(user => !!user)
21 .do(loggedIn => {
22 if (!loggedIn) {
23 console.log("access denied")
24 this.router.navigate(['/login']);
25 }
26 })
27
28 }
29 }
In the routing module, you can activate the guard by adding it to the canActivate property.
First, let’s start with this shell of a component to handle the file uploading process.
Problem
You want to initiate an Upload task.
Solution
Important Caveat
The path to a file in a storage bucket must be unique. If two users upload a file to
/images/my_pet_pug.jpg, only the first file will be persisted. If this could be a problem
with your file structure, you may want to add a unique token or timestamp to every file
name.
An upload task is a promise to store a file in Firebase Storage. You create the task like so:
1. Get a JavaScript File object via a form input (See Section 5.4)
2. Make a reference to the location you want to save it in Firebase
3. Call the put method to return an upload task Promise.
1 upload(file: File) {
2 const storageRef = firebase.storage().ref().child('/images');
3 return storageRef.put(file);
4 }
Problem
You want to handle the progress, success, and failure of the upload task.
Solution
Let’s modify the example in 5.1. Firebase has a TaskState object, with only one event - STATE_-
CHANGED.
Firebase Cloud Storage 63
1 upload(file: File) {
2 const storageRef = firebase.storage().ref().child('images');
3 const uploadTask = storageRef.put(file);
4
5 return uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED,
6 (snapshot) => {
7 // upload in progress
8 console.log(snapshot)
9 },
10 (error) => {
11 // upload failed
12 console.log(error)
13 },
14 () => {
15 // upload success
16 console.log("success")
17 }
18 );
19 }
Problem
You want to save properties from an uploaded file to the realtime database.
Solution
Saving upload information to the database is almost always required, as you will want to probably
reference the download URL. Here’s what we can get from a file snapshot.
https://firebase.google.com/docs/reference/js/firebase.storage.UploadTaskSnapshot
• downloadURL
• totalBytes
• metadata (contentType, contentLanguage, etc)
When the upload task completes, we can use the snapshot to save information to the database. Again,
we are building on the upload function in examples 5.1 and 5.2.
Firebase Cloud Storage 64
1 upload(file: File) {
2
3 // omitted see 5.2...
4 () => {
5 // upload success
6 this.saveDownloadUrl(uploadTask.snapshot.downloadURL)
7 }
8 }
9
10 saveDownloadUrl(url) {
11 this.db.list('images')
12 .push({ downloadUrl: url })
13 }
In Angular, you can use the url to display images or links to access downloadable files.
1 <img [src]="someImage.downloadUrl">
Problem
You want to enable users to upload a single file from Angular.
Solution
Now that you know how to upload files on the backend, how do you actually receive the necessary
File object from a user?
Here we have an input element for a file, that triggers a detectFiles function when it changes
(when they select a file on their device). Then the user can start the upload process by clicking the
button attached to uploadSingle.
Now let’s define these event handlers in the TypeScript. The change event on the form input will
contain a FileList, which can be obtained with $event.target.files. When the upload button is
clicked, the file is sent to Firebase with upload function from section 5.1.
Firebase Cloud Storage 65
1 selectedFiles: FileList;
2
3 detectFiles($event) {
4 this.selectedFiles = $event.target.files;
5 }
6
7 uploadSingle() {
8 let file: File = this.selectedFiles.item(0)
9 this.uploadTask = this.upload(file)
10 }
Problem
You want users to upload multiple files at a time.
Solution
Uploading multiple files can be done with a few slight changes. In the template, the form input needs
to include the multiple attribute.
Firebase Cloud Storage 66
To loop over the FileList, we bring in Lodash (_.each and _.range). A FileList object is not an
array that you can just iterate over, but you can iterate over a range of integers based on its length.
1 selectedFiles: FileList;
2
3 detectFiles($event) {
4 this.selectedFiles = $event.target.files;
5 }
6
7 uploadMultiple() {
8 let filesIndex = _.range(files.length)
9
10 _.each(filesIndex, (idx) => {
11 this.upload(selectedFiles[idx])
12 })
13 }
Problem
You want users to be able to delete their files.
Solution
Deleting files follows the same process as uploading, but you need to know the location of the file. In
most cases, this means you should have the image name or path saved in the database. Let’s imagine
looping through some images in the database.
Now, we can pass that image name to a storage reference and delete it.
Firebase Cloud Storage 67
1 deleteFile(name) {
2 firebase.storage()
3 .ref(`/images`)
4 .child(name).delete()
5 }
Problem
You want to alert a user when their file is not valid.
Solution
You should always validate files on the frontend because it creates a better user experience (but
validate the backend also, see the next section). To do this, we use the built-in File object in
javascript to collect some useful information about the file blob. The size and type attributes are
probably the most common for validation.
1 validateFile(file: File) {
2 const sizeMb = file.size / 1024 / 1024
3 const mimeType = file.type.split('/')[0]
4
5 validationErrors = []
6 const sizeError = "Must be less than 10 Megabytes"
7 const mimeError = "Must be an image"
8
9 if (sizeMb > 10) validationErrors.push(sizeError)
10 if (mimeType != 'image') validationErrors.push(mimeError)
11
12 return validationErrors
13 }
Problem
You want to show users the progress of their upload.
Firebase Cloud Storage 68
Solution
If you recall example 5.2, the upload task will return a snapshot of the upload after each state
change event. The snapshot has two properties we can use to build a progress bar - totalBytes
and bytesTransferred. Just apply some simple math to convert them to a percentage.
1 currentProgress: number;
2
3 upload {
4 return uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED,
5 (snapshot) => {
6 this.currentProgress = (
7 snapshot.bytesTransferred / snapshot.totalBytes
8 ) * 100
9 },
10 /// omitted... see 5.2
11 )
12 }
So now we have the percentage that will be updated on the component after each upload task event.
Simply pass the currentProgress percentage as the value to the HTML progress element.
Problem
You want to put a base64 encoded file into storage
Solution
You might have images encoded as a Base64 to avoid depending on an external file. There is no need
to convert it - you can still upload it via putString, which also returns an upload task Promise.
Firebase Cloud Storage 69
1 uploadBase64() {
2 const imageString = '5c6p7Y+2349X44G7232323...'
3 return firebase.storage().ref('/images')
4 .putString(imageString)
5 }
Problem
You want to prevent users from uploading extremely large files or certain file types to your storage
bucket.
Solution
Backend validation is extremely important when dealing with file uploads from users. File storage
rules are similar to database rules in principle, but use a slightly different syntax.
Here’s the default security settings in Firebase. Users can only read/write if they are logged in.
1 service firebase.storage {
2 match /b/{bucket}/o {
3 match /{allPaths=**} {
4 allow read, write: if request.auth != null;
5 }
6 }
7 }
Let’s authorize writes for the image owner only, but allow any user to read the images.
1 match /images/{userId}/{allImages=**} {
2 allow read;
3 allow write: if (request.auth.uid == userId);
4 }
Now let’s validate file size and type. It must be less than 10 Mb and have an image MIME type.
Firebase Cloud Storage 70
1 match /{imageId} {
2 allow read;
3 allow write: if request.resource.size < 10 * 1024 * 1024
4 && request.resource.contentType.matches('image/.*')
1 match /b/bucket-PUBLIC.appspot.com/o {
2 match /{allPaths=**} {
3 allow read, write;
4 }
5 }
6
7 match /b/bucket-PRIVATE.appspot.com/o {
8 match /{allPaths=**} {
9 allow read, write: if request.auth != null;
10 }
11 }
Firebase Cloud Functions
Cloud functions are Functions-as-a-Service (FaaS) that allow you to run code on demand without
ever worrying about server deployment.
• No server management
• Isolated codebase
• Billed on demand
When you deploy to a Platform-as-a-Service (PaaS), such as Heroku, you are billed a monthly rate
even if the volume is miniscule. I find it annoying to pay $X per month for a background task that
only runs 500 times per month.
The great thing about Cloud Functions is that you’re billed by the millisecond. Your function runs
for 400ms, then you’re billed $0.00001 or whatever the actual cost.
It’s also really helpful to isolate code outside of Angular, because you really need your Angular app
to stay lean and agile. If you think of an app like a retail store, Angular is the customer service team
and the cloud functions are the warehouse workers. The reps need to be available quickly and offer
a responsive and engaging experience. Meanwhile, the warehouse workers need to handle all the
heavy lifting and maintenance behind the scenes.
Problem
You want to initialize cloud functions in your project
Solution
Cloud functions are managed with the firebase-tools CLI.
Run firebase init, choose functions and install dependencies, then cd functions and npm in-
stall.
Firebase Cloud Functions 72
From there, you have an isolated NodeJS environment to build microservices, setup HTTP endpoints,
and run background tasks. You may also need to save environment variables, such as API keys.
firebase functions:config:set someApiKey="XYZ"
You can access your environment variables by calling functions.config().someApiKey inside the
environment.
The index.js file is where you will define the function. Most commonly, you will import the
admin database to override any read/write/validate rules (see 2.14). You define functions calling
exports.functionName, which we will see in the upcoming examples in this chapter.
Problem
You want to deploy your cloud functions.
Solution
Let’s deploy the function from 6.1. It’s as simple as:
firebase deploy --only functions
Firebase should have returned the endpoint URL to trigger this function. Let’s hit with cURL, then
check the logs to see if it’s working.
Firebase Cloud Functions 73
Tip: If you have a custom domain in your project, requests can be proxied to that domain when
calling HTTP functions.
Problem
You want to create a cloud function that is triggered over HTTP.
Solution
An HTTP cloud function will allow you arbitrarily execute code from any event, such as a button
click, form submission, etc. It gives you an API endpoint without the need to manage a backend
server.
HTTP cloud functions have a request req and a response res. In most cases, you will parse the
request parameters, then finish by calling response.send() to send JSON back to the requester.
In this example, the HTTP function returns a word count in JSON format for a specific book in the
database. It makes a reference to the database and calls once('value') - this will return a single
snapshot of the data at this location. From there, we can parse the data into a word count object,
convert it to JSON, then send the response.
Firebase Cloud Functions 74
1 exports.bookWordCount = functions.https
2 .onRequest((req, res) => {
3
4 const bookId = req.body.bookId
5
6 if (!bookId) return;
7
8 return admin.database()
9 .ref(`/books/${bookId}`)
10 .once('value')
11 .then(data => {
12 return data.val()
13 })
14 .then( book => {
15 const wordCount = book.content.split(' ').length;
16 const json = JSON.stringify({ words: wordCount })
17 res.status(200).send(json)
18 })
19
20 })
Problem
You want to trigger a function when a user signs up or deletes their account.
Solution
Firebase offers two triggers for authentication events of onCreate and onDelete. Common use cases
for the onCreate event could be sending a transactional email or updating a notification feed. The
onDelete function can be used to delete a user’s data when they close their account.
1 exports.deleteUserData = functions.auth
2 .user()
3 .onDelete(event => {
4
5 const userId = event.data.uid;
6 const email = event.data.email;
7
8 return admin.database()
9 .ref(`users/${userId}`).remove();
10
11 });
Problem
You want to update a user’s notification feed when their content is liked.
Solution
Database triggers are the most useful type of Firebase Cloud Functions because they solve many
common background situations that are impractical/difficult to perform in Angular.
You invoke functions by referencing a specific point in the database, then specify the type of
operation trigger. There are four possible triggers.
onWrite() - All operations onCreate() - New data created onUpdate() - Existing data updated
onDelete() - Data removed
In this example, we will update the user’s toast notification feed when they gain a new follower.
Firebase Cloud Functions 76
1 exports.sendToast = functions.database
2 .ref('/followers/{userId}/{username}')
3 .onCreate(event => {
4
5 const data = event.data.val()
6 const userId = event.params.userId
7 const follower = event.params.username
8
9 const message = { message: `You've been followed by ${username}` }
10
11 return admin.database()
12 .ref(`toasts/${userId}`)
13 .push(message);
14
15 });
Problem
You want to trigger a cloud function when data changes in a Firestore document.
Solution
The cloud function triggers are identical for Firestore and the Realtime DB, just to recap:
onWrite() - All operations onCreate() - New document created onUpdate() - Existing document
updated onDelete() - Document removed
Because Firestore is so similar to the Realtime DB, I just going to highlight the important differences.
It really just boils down to slightly different terminology:
1 exports.myFunctionName = functions.firestore
2 .document('books/bookID').onUpdate((event) => {
3
4 // Current data state
5 const data = event.data.data();
6
7 // Data state before the update
8 const previousData = event.data.previous.data();
9
10 // Update data on the document
11 return event.data.ref.update({
12 hello: 'world'
13 });
14 });
Problem
You want to resize images uploaded to firebase storage into thumbnails of various sizes.
Solution
Storage functions are similar to the database functions, but you only have one trigger onChange(),
which fires on both create and delete. File objects have resourceState metadata, or ‘exists’, or
‘not_exists’ to distinguish between create and remove.
This final cloud function is by far the most complex. I wanted to demonstrate what a fully fledged,
relatively complex, cloud function can look like. Here we are using the sharp NPM package to resize
the image, save it to the function’s local storage on the underlying virtual instance, then upload it
to Firebase.
10 .onChange(event => {
11
12 const object = event.data; // storage object
13
14 const fileBucket = object.bucket;
15 const filePath = object.name;
16 const contentType = object.contentType;
17 const resourceState = object.resourceState;
18
19 const SIZES = [64, 256, 512]; // Resize pixel targets
20
21 if (!contentType.startsWith('image/') || resourceState == 'not_exists') {
22 console.log('This is not an image.');
23 return;
24 }
25
26 if (_.includes(filePath, '_thumb')) {
27 console.log('already processed image');
28 return;
29 }
30
31
32 const fileName = filePath.split('/').pop();
33 const bucket = gcs.bucket(fileBucket);
34 const tempFilePath = path.join(os.tmpdir(), fileName);
35
36 return bucket.file(filePath).download({
37 destination: tempFilePath
38 }).then(() => {
39
40 _.each(SIZES, (size) => {
41
42 let newFileName = `${fileName}_${size}_thumb.png`
43 let newFileTemp = path.join(os.tmpdir(), newFileName);
44 let newFilePath = `/thumbs/${newFileName}`
45
46 sharp(tempFilePath)
47 .resize(size, null)
48 .toFile(newFileTemp, (err, info) => {
49
50 bucket.upload(newFileTemp, {
51 destination: newFilePath
Firebase Cloud Functions 79
52 });
53 });
54 })
55 })
56 })
Real World Combined Examples
Now it’s time to bring everything together. In this section, I solve several real-world problems
by combining concepts from the Firestore, Realtime Database, user auth, storage, and functions
chapters.
I’ve selected these examples because they are (A) commonly needed by developers and (B)
implement many of the examples covered in this book. Each example also has a corresponding
video lesson.
Firestore OAuth
https://youtu.be/e8GA1UOj8mE
Problem
You want to maintain custom user records that go beyond the basic information provided by Firebase
authentication.
Solution
There’s a good chance you want to keep track of more than just an email address, display name, and
userID.
NgModule Setup
Although technically optional, it is a good design pattern to keep your authentication setup in a core
module. The purpose of a core module is to provide services that your app will use globally, such as
authentication, logging, toast messages, etc.
1 ng g module core
Add the AngularFire Firestore and Auth modules to your core module.
Real World Combined Examples 81
1 // core.module.ts
2 import { NgModule } from '@angular/core';
3 import { AuthService } from './auth.service';
4 import { AngularFireAuthModule } from 'angularfire2/auth';
5 import { AngularFirestoreModule } from 'angularfire2/firestore';
6 @NgModule({
7 imports: [
8 AngularFireAuthModule,
9 AngularFirestoreModule
10 ],
11 providers: [AuthService]
12 })
13 export class CoreModule { }
1 // app.module.ts
2
3 import { CoreModule } from './core/core.module';
4 // ...
5 @NgModule({
6 // ... omitted
7 imports: [
8 BrowserModule,
9 AppRoutingModule,
10 AngularFireModule.initializeApp(yourConfig),
11 CoreModule // <-- add core module
12 ]
13 })
14 export class AppModule { }
Auth Service
The auth service is where most of the magic happens. It facilitates the sign-in process, watches the
user session, and allows us to save custom user data to the Firestore database. Here’s a breakdown
of the important steps in the service code.
• interface User: The interface declares the properties of the custom user object. Feel free to
add any custom data you want here to extend the basic Firebase auth data.
• constructor(): The constructor will set the Observable. First it receives the current Firebase
auth state. If present, it will hit up Firestore for the user’s saved custom data. If null, it will
return an Observable.of(null).
Real World Combined Examples 82
• googleLogin(): This method triggers the popup window that authenticates the user with their
Google account. It returns a Promise that resolves with the auth credential. The oAuthLogin()
method is useful if you have multiple OAuth options because it can be reused with different
providers.
• updateUserData(): This private method runs after the user authenticates and sets their
information to the Firestore database.
36 return Observable.of(null)
37 }
38 })
39 }
40
41 googleLogin() {
42 const provider = new firebase.auth.GoogleAuthProvider()
43 return this.oAuthLogin(provider);
44 }
45
46 private oAuthLogin(provider) {
47 return this.afAuth.auth.signInWithPopup(provider)
48 .then((credential) => {
49 this.updateUserData(credential.user)
50 })
51 }
52
53
54 private updateUserData(user) {
55 // Sets user data to firestore on login
56
57 const userRef: AngularFirestoreDocument<any> = this.afs.doc(`users/${user.ui\
58 d}`);
59
60 const data: User = {
61 uid: user.uid,
62 email: user.email,
63 displayName: user.displayName,
64 photoURL: user.photoURL
65 }
66
67 return userRef.set(data, { merge: true })
68
69 }
70
71
72 signOut() {
73 this.afAuth.auth.signOut().then(() => {
74 this.router.navigate(['/']);
75 });
76 }
77 }
Real World Combined Examples 84
1 ng g guard core/auth
The next thing we want to do is protect our routes from unauthenticated users. Now that we have
an Observable in the service, we can handle this task easily with a canActivate guard. We just pass
it the Observable of the user from the auth service. If it emits true, the route can be accessed. If false,
the user is redirected to the login page.
You can use the guard in your router by simply adding it to the canActivate array for a given route,
for example:
Real World Combined Examples 85
As a final step, let’s create a user profile so you can see how to use the user Observable in the HTML.
The component TypeScript just needs to have the auth service injected as a public property in the
constructor.
In the component HTML, we have two separate templates that are shown conditionally based on
the user Observable data. If it’s null, we show the guest template, but if it’s present we can show
the authenticated template and corresponding user data.
I have also added a Google Login button to the profile, but you might consider making it a standalone
component that you can use outside of the user profile.
Real World Combined Examples 86
Problem
You want to create your own five-star-review system (like Yelp!) with Firestore from scratch.
Real World Combined Examples 87
Solution
Knowing how to implement star reviews is an important skill for a developer because the same
concepts are used for likes, hearts, votes, and many other common UX features.
How do we model star-ratings in a NoSQL database like firestore? In the SQL world, this is known
as a many-to-many-through‘ relationship where Users have many Movies through Reviews AND
Movies have many Users through Reviews
In the diagram above, we can see how the movies collection and users collection have a two-way
connection through the middle-man stars collection. All data about a relationship is kept in the star
document - data never needs to change on the connected user/movie documents directly.
Having a root collection structure allows us to query both “Movie reviews” and “User reviews”
independently. This would not be possible if stars were nested as a sub collection.
For this demonstration, I am going to manually create a few user documents and a movie document.
As you will see, the StarService is completely independent, so you can easily drop it into your app.
All you need is a reference to the two AngularFirestoreDocument objects that you want to connect.
Here’s what the database collections and documents look like in Firestore.
In the app component, we will make a reference to the current user and movie. In the real world, you
would get your user from an auth service. Movies might come from a URL param or a collection of
movies. These issues are not directly relevant to our star feature, so I am simply hard coding them
in the app component.
I also created a couple of getters to retrieve the document ID from the AngularFirestoreDocument.
32
33 get userId() {
34 return this.userDoc.ref.id
35 }
36
37 }
The typescript getters will allow us to conveniently pass the document ids to a child component,
which we are going to build later in the lesson.
The star service will handle interaction with the stars collection in the Firestore back-end database.
1 ng g component star-review
You can drop the star review component into any other component that has access to a movie
reference. It acts as a child component that will display/update reviews for its parent component -
in this case movies.
The star review component will perform the following tasks.
The star service only requires a userId and movieId, therefore we can pass these values from the
parent component using the @Input decorator.
Real World Combined Examples 91
To implement the clickable star buttons, we need to style radio buttons as star icons. In this case, we
have 10 clickable radio buttons ranging from 0.5 to 5 stars. When clicked buy the user, it will trigger
the starHandler(val) method and update the corresponding data in Firestore.
Rather than code 10 different inputs, I loop over 5 integers and wrap the full-star and half-star in an
ng-container - that reduces the HTML code by ∼80%.
Real World Combined Examples 92
Note: It’s important that the the id on the input matches the for property on the label.
1 <h3>Average Rating</h3>
2
3 {{ avgRating | async }}
4
5
6 <h3>Reviews</h3>
7 <div *ngFor="let star of stars | async">
8
9 {{ star.userId }} gave {{ star.movieId }} {{ star.value }} stars
10
11 </div>
12
13
14 <h3>Post your Review</h3>
15
16 <fieldset class="rating">
17 <ng-container *ngFor="let num of [5, 4, 3, 2, 1]">
18 <!-- full star -->
19 <input (click)="starHandler(num)"
20 [id]="'star'+num"
21 [value]="num-0.5"
22 name="rating"
23 type="radio" />
24
25 <label class="full" [for]="'star'+num"></label>
26
27 <!-- half star -->
28 <input (click)="starHandler(num-0.5)"
29 [value]="num-0.5"
30 [id]="'halfstar'+num"
31 name="rating"
32 type="radio" />
33
34 <label class="half" [for]="'halfstar'+num"></label>
35
36 </ng-container>
37 </fieldset>
Lastly, I wanted to include the CSS that makes the star UI possible, originally used in this CodePen4 .
Note: you will also need FontAwesome icons installed in your project for the CSS to work properly.
4
https://codepen.io/jamesbarnett/pen/vlpkh
Real World Combined Examples 93
Problem
You want to give firebase users custom usernames, then validate them asynchronously after each
keypress.
Solution
Get started by implementing the user authentication paradigm of your choice from chapter 3.
You will notice that Firebase authentication is convenient, but you cannot assign custom usernames
out of the box. In this example, we are going to give users custom usernames and asynchronously
validate their availability during the signup process. On every keyup event, the username will be
checked against the database for duplicates.
When a user signs up, the app will look for a username. If it is missing, Angular will keep them on
the login component and require a username to be entered, then validate its availability by querying
the database for a match.
The database has a collection of users for basic record keeping. However, the quickest way to
asynchronously check username availability is to save all usernames in their own collection, with
the keys being the usernames that are not available to new users. The database structure looks like
this:
1 -|users
2 -|$authUID
3 username: "jeffd23"
4
5 -|usernames
6 jeffd23: $authUID
Real World Combined Examples 95
First, let’s create a User class to simplify the auth object. We only care about the uid and the
username. As a constructor, it will take the Firebase AuthState from AngularFire2.
We need to subscribe to both the Firebase auth object and the extra user information (i.e. username)
in the database at the same time. So, how do we handle nested subscriptions with RxJS? In this case,
we are going to use switchMap, which will emit the Firebase auth object, then get the user values
from the database, keeping everything packaged up as an Observable. Create a new service to handle
this logic with ng generate service auth.
auth.service.ts
33 .subscribe(user => {
34 this.currentUser['username'] = user.username;
35 })
36
37 }
38
39 googleLogin() {
40 const provider = new firebase.auth.GoogleAuthProvider()
41 return this.afAuth.auth.signInWithPopup(provider)
42 }
43 }
Now it’s time to verify the availability of the selected username. First, the username collection is
queried with the user’s text input. Querying with this method only targets a single key value pair,
rather than an entire list, making it much more efficient. We also use a TypeScript getter to determine
if the current user already has a username. Here’s how the remainder of the service should look:
1 get hasUsername() {
2 return this.currentUser.username ? true : false
3 }
4
5 checkUsername(username: string) {
6 username = username.toLowerCase()
7 const ref = this.db.object(`usernames/${username}`)
8 return ref.valueChanges()
9 }
10
11 updateUsername(username: string) {
12 let data = {}
13 data[username] = this.currentUser.uid
14
15 this.db.object(`/users/${this.currentUser.uid}`)
16 .update({"username": username})
17
18 this.db.object(`/usernames`)
19 .update(data)
20 }
The service is ready to go - let’s wire up the component. It runs the query to Firebase after each
keydown event to determine if a matching username exists. If not, the user can go ahead and select
it. Generate a component with ng generate component user-login, then inject the auth service.
user-login.component.ts
Real World Combined Examples 97
user-login.component.html
In the template, these event handlers are bound to the corresponding elements.
Real World Combined Examples 98
1 <h1>Login</h1>
2 <button (click)="signInWithGoogle()"
3 *ngIf="!auth.currentUser">
4 Connect Google
5 </button>
6
7 <button *ngIf="auth.currentUser"
8 (click)="logout()">
9 Logout
10 </button>
11
12 <div *ngIf="auth.currentUser && !auth.hasUsername">
13
14 <h3>Choose a Username</h3>
15
16 <input type="text"
17 [(ngModel)]="usernameText"
18 (keyup)="checkUsername()">
19
20 <p *ngIf="usernameAvailable && usernameText">
21 Success!! @{{usernameText}} is available
22 </p>
23
24 <p *ngIf="!usernameAvailable && usernameText">
25 Danger!! @{{usernameText}} has already been taken
26 </p>
27
28 <button [disabled]="!usernameAvailable || !usernameText"
29 (click)="updateUsername()">
30 Select Username1
31 </button>
32 </div>
database.rules.json
Just one last thing to consider. The current username validation is great as a frontend UX feature,
but it is still vulnerable to accidental duplication. Let’s add an extra layer of security by creating a
Firebase database rule that ensures a username cannot be accidentally duplicated on the backend.
Real World Combined Examples 99
1 "users": {
2 ".write": "auth != null",
3 "username": {
4 ".validate": "!root.child('usernames').child(newData.val()).exists()"
5 }
6 },
7 "usernames": {
8 ".write": "auth != null"
9 }
Problem
You want to collect payments from customers with Stripe.
Solution
The final example will use the Stripe Payments API to collect credit card information from customers
(without sensitive information ever touching your app), then charge the card on the backend with
a Firebase Cloud Function.
First things first, head over to stripe.com and obtain an API key.
Let’s review how the payment process works with Stripe, Angular, and Firebase.
Real World Combined Examples 100
Initial Setup
We need the stripe checkout library in our project. Stripe recommends making this script global to
enable better fraud detection, so let’s add it the head of the index.html. In fact, it must be loaded
directly from Stipe – using a local NPM package is not supported.
1 <head>
2 <script src="https://checkout.stripe.com/checkout.js"></script>
3 </head>
Lastly, you should add your Stripe API keys to your environment files. Add the test key to the
environment.ts file.
Database Design
Stripe returns the data for the token and charge in JSON. Each payment will be nested under its
associated userId in the database.
1 payments
2 $userId
3 $paymentId
4 amount: number
5 token: object (from Stripe)
6 charge: object (from Stripe)
make-payment.component.html
The payment component’s html is very simple. Just a button to trigger the stripe checkout window.
Real World Combined Examples 101
1 <button (click)="handlePayment()">
2 Add Credits to your Account
3 </button>
make-payment.component.ts
Now we need to modify the Stripe Custom Integration5 to work with Angular. When stripe returns
the token, we save it to Firebase using the service defined in the next step.
Note: Stripe amounts are equal to 1/100th of the base currency, so an amount of 500 equals $5.00.
32 });
33 }
34
35 @HostListener('window:popstate')
36 onPopstate() {
37 this.handler.close()
38 }
39
40 }
payment.service.ts
The service just needs to get the current user’s ID, then save the Stripe token to the database.
At this point, the card has NOT actually been charged. We just have a token that needs to be
processed on a backend server.
Follow these steps to initialize Firebase Cloud Functions in your project and set an environment
variable for your Stripe API key.
Note: You can skip firebase init functions if your project is already configured with Firebase
Cloud Functions.
Real World Combined Examples 103
index.js
The function will be triggered during the onWrite() database event. When data is written the
/payments/{userId}/{paymentId} reference point, it will trigger the function below. It chains
together three separate promises that do the following:
27
28 const amount = payment.amount;
29 const idempotency_key = paymentId; // prevent duplicate charges
30 const source = payment.token.id;
31 const currency = 'usd';
32 const charge = {amount, currency, source};
33
34
35 return stripe.charges.create(charge, { idempotency_key });
36
37 })
38
39 .then(charge => {
40 admin.database()
41 .ref(`/payments/${userId}/${paymentId}/charge`)
42 .set(charge)
43 })
44
45 });
Now deploy the function with firebase deploy --only functions and your app is ready to start
accepting payments.
Angular has a realtime connection with Firebase, so the user will automatically see the charge details
updated clientside, assuming they are subscribed to this data.
The End
That’s it! My hope is that this book serves as a useful reference in your Angular Firebase app
development journey. Things change quickly in the web development world, so make sure stay
up to date with the latest trends.