Tag: Firebase

User Registration and Auth Using Firebase and React

The ability to identify users is vital for maintaining the security of any applications. Equally important is the code that’s written to manage user identities, particularly when it comes to avoiding loopholes for unauthorized access to data held by an application. Writing authentication code without a framework or libraries available can take a ton of time to do right — not to mention the ongoing maintainance of that custom code.

This is where Firebase comes to the rescue. Its ready-to-use and intuitive methods make setting up effective user identity management on a site happen in no time. This tutorial will work us through on how to do that: implementing user registration, verification, and authentication.

Firebase v9 SDK introduces a new modular API surface, resulting in a change to several of its services, one of which is Firebase Authentication. This tutorial is current to the changes in v9.

To follow along with this tutorial, you should be familiar with React, React hooks, and Firebase version 8. You should also have a Google account and Node installed on your machine.

Table of Contents

Setting up Firebase

Before we start using Firebase for our registration and authentication requirements, we have to first set up our Firebase project and also the authentication method we’re using.

To add a project, make sure you are logged into your Google account, then navigate to the Firebase console and click on Add project. From there, give the project a name (I’m using “Firebase-user-reg-auth”) and we should be all set to continue.

You may be prompted to enable Google Analytics at some point. There’s no need for it for this tutorial, so feel free to skip that step.

Firebase has various authentication methods for both mobile and web, but before we start using any of them, we have to first enable it on the Firebase Authentication page. From the sidebar menu, click on the Authentication icon, then, on the next page, click on Get started.

We are going to use Email/Password authentication. Click on it and we will be prompted with a screen to enable it, which is exactly what we want to do.

Cloning and setting up the starter repo

I have already created a simple template we can use for this tutorial so that we can focus specifically on learning how to implement the functionalities. So what we need to do now is clone the GitHub repo.

Fire up your terminal. Here’s what we can run from the command line:

git clone -b starter https://github.com/Tammibriggs/Firebase_user_auth.git  cd Firebase_user_auth  npm install

I have also included Firebase version 9 in the dependency object of the package.json file. So, by running the npm install command, Firebase v9 — along with all other dependencies — will be installed.

With done that, let’s start the app with npm start!

Integrating Firebase into our React app

To integrate Firebase, we need to first get the web configuration object and then use it to initialize Firebase in our React app. Go over to the Firebase project page and we will see a set of options as icons like this:

Click on the web (</>) icon to configure our Firebase project for the web, and we will see a page like this:

Enter firebase-user-auth as the name of the web app. After that, click on the Register app button, which takes us to the next step where our firebaseConfig object is provided.

Copy the config to the clipboard as we will need it later on to initialize Firebase. Then click on the Continue to console button to complete the process.

Now, let’s initialize Firebase and Firebase Authentication so that we can start using them in our app. In the src directory of our React app, create a firebase.js file and add the following imports:

// src/firebase.js import { initializeApp } from 'firebase/app' import {getAuth} from 'firebase/auth'

Now, paste the config we copied earlier after the imports and add the following lines of code to initialize Firebase and Firebase Authentication.

// src/firebase.js const app = initializeApp(firebaseConfig) const auth = getAuth(app)  export {auth}

Our firebase.js file should now look something like this:

// src.firebase.js import { initializeApp } from "firebase/app" import { getAuth } from "firebase/auth"  const firebaseConfig = {   apiKey: "API_KEY",   authDomain: "AUTH_DOMAIN",   projectId: "PROJECT_ID",   storageBucket: "STORAGE_BUCKET",   messagingSenderId: "MESSAGING_SENDER_ID",   appId: "APP_ID" }  // Initialize Firebase and Firebase Authentication const app = initializeApp(firebaseConfig) const auth = getAuth(app) export {auth}

Next up, we’re going to cover how to use the ready-to-use functions provided by Firebase to add registration, email verification, and login functionality to the template we cloned.

Creating User Registration functionality

In Firebase version 9, we can build functionality for user registration with the createUserWithEmailAndPassword function. This function takes three arguments:

  • auth instance/service
  • email
  • password

Services are always passed as the first arguments in version 9. In our case, it’s the auth service.

To create this functionality, we will be working with the Register.js file in the src directory of our cloned template. What I did in this file is create three form fields — email, password, and confirm password — and input is controlled by the state. Now, let’s get to business.

Let’s start by adding a function that validates the password and confirm password inputs, checking if they are not empty and are the same: Add the following lines of code after the states in the Register component:

// src/Register.js // ...  const validatePassword = () => {   let isValid = true   if (password !== '' && confirmPassword !== ''){     if (password !== confirmPassword) {       isValid = false       setError('Passwords does not match')     }   }   return isValid }  // ...

In the above function, we return an isValid variable which can return either true or false based on the validity of the passwords. Later on, we will use the value of this variable to create a condition where the Firebase function responsible for registering users will only be invoked if isValid is true.

To create the registration functionality, let’s start by making the necessary imports to the Register.js file:

// src/Register.js import {auth} from './firebase' import {createUserWithEmailAndPassword} from 'firebase/auth'

Now, add the following lines of code after the validatePassword password function:

// src/Register.js // ...  const register = e => {   e.preventDefault()   setError('')   if(validatePassword()) {     // Create a new user with email and password using firebase       createUserWithEmailAndPassword(auth, email, password)       .then((res) => {           console.log(res.user)         })       .catch(err => setError(err.message))   }   setEmail('')   setPassword('')   setConfirmPassword('') }  // ...

In the above function, we set a condition to call the createUserWithEmailAndPassword function only when the value returning from validatePassword is true.

For this to start working, let’s call the register function when the form is submitted. We can do this by adding an onSubmit event to the form. Modify the opening tag of the registration_form to look like this:

// src/Register.js <form onSubmit={register} name='registration_form'>

With this, we can now register a new user on our site. To test this by going over to http://localhost:3000/register in the browser, filling in the form, then clicking on the Register button.

Showing a user registration form with fields to enter an email. a password, and password confirmation. A gray button labeled Register is below the three stacked fields.

After clicking the Register button, if we open the browser’s console we will see details of the newly registered user.

Managing User State with React Context API

Context API is a way to share data with components at any level of the React component tree without having to pass it down as props. Since a user might be required by a different component in the tree, using the Context API is great for managing the user state.

Before we start using the Context API, there are a few things we need to set up:

  • Create a context object using the createContext() method
  • Pass the components we want to share the user state with as children of Context.Provider
  • Pass the value we want the children/consuming component to access as props to Context.Provider

Let’s get to it. In the src directory, create an AuthContext.js file and add the following lines of code to it:

// src/AuthContext.js import React, {useContext} from 'react'  const AuthContext = React.createContext()  export function AuthProvider({children, value}) {   return (     <AuthContext.Provider value={value}>       {children}     </AuthContext.Provider>   ) }  export function useAuthValue(){   return useContext(AuthContext) }

In the above code, we created a context called AuthContext along with that we also created two other functions that will allow us to easily use the Context API which is AuthProvider and useAuthValue.

The AuthProvider function allows us to share the value of the user’s state to all the children of AuthContext.Provider while useAuthValue allows us to easily access the value passed to AuthContext.Provider.

Now, to provide the children and value props to AuthProvider, modify the App.js file to look something like this:

// src/App.js // ... import {useState} from 'react' import {AuthProvider} from './AuthContext'  function App() {   const [currentUser, setCurrentUser] = useState(null)    return (     <Router>       <AuthProvider value={{currentUser}}>         <Switch>          ...         </Switch>       </AuthProvider>     </Router>   ); }  export default App;

Here, we’re wrapping AuthProvider around the components rendered by App. This way, the currentUser value supplied to AuthProvider will be available for use by all the components in our app except the App component.

That’s it as far as setting up the Context API! To use it, we have to import the useAuthValue function and invoke it in any of the child components of AuthProvider, like Login. The code looks something like this:

import { useAuthValue } from "./AuthContext"  function childOfAuthProvider(){   const {currentUser} = useAuthValue()   console.log(currentUser)    return ... }

Right now, currentUser will always be null because we are not setting its value to anything. To set its value, we need to first get the current user from Firebase which can be done either by using the auth instance that was initialized in our firebase.js file (auth.currentUser), or the onAuthStateChanged function, which actually happens to be the recommended way to get the current user. That way, we ensure that the Auth object isn’t in an intermediate state — such as initialization — when we get the current user.

In the App.js file, add a useEffect import along with useState and also add the following imports:

// src/App.js import {useState, useEffect} from 'react' import {auth} from './firebase' import {onAuthStateChanged} from 'firebase/auth'

Now add the following line of code after the currentUser state in the App component:

// src/App.js // ...  useEffect(() => {   onAuthStateChanged(auth, (user) => {     setCurrentUser(user)    }) }, [])  // ...

In the above code, we are getting the current user and setting it in the state when the component renders. Now when we register a user the currentUser state will be set with an object containing the user’s info.

Send a verification email to a registered user

Once a user is registered, we want them to verify their email address before being able to access the homepage of our site. We can use the sendEmailVerification function for this. It takes only one argument which is the object of the currently registered user. When invoked, Firebase sends an email to the registered user’s email address with a link where the user can verify their email.

Let’s head over to the Register.js file and modify the Link and createUserWithEmailAndPassword import to look like this:

// src/Register.js import {useHistory, Link} from 'react-router-dom' import {createUserWithEmailAndPassword, sendEmailVerification} from 'firebase/auth'

In the above code, we have also imported the useHistory hook. This will help us access and manipulate the browser’s history which, in short, means we can use it to switch between pages in our app. But before we can use it we need to call it, so let’s add the following line of code after the error state:

// src/Register.js // ... const history = useHistory()  // ...

Now, modify the .then method of the createUserWithEmailAndPassword function to look like this:

// src/Register.js // ... .then(() => {   sendEmailVerification(auth.currentUser)   .then(() => {     history.push('/verify-email')   }).catch((err) => alert(err.message)) }) // ...

What’s happening here is that when a user registers a valid email address, they will be sent a verification email, then taken to the verify-email page.

There are several things we need to do on this page:

  • Display the user’s email after the part that says “A verification email has been sent to:”
  • Make the Resend Email button work
  • Create functionality for disabling the Resend Email button for 60 seconds after it is clicked
  • Take the user to their profile page once the email has been verified

We will start by displaying the registered user’s email. This calls for the use of the AuthContext we created earlier. In the VerifyEmail.js file, add the following import:

// src/VerifyEmail.js import {useAuthValue} from './AuthContext'

Then, add the following code before the return statement in the VerifyEmail component:

// src/VerifyEmail.js const {currentUser} = useAuthValue()

Now, to display the email, add the following code after the <br/> tag in the return statement.

// src/VerifyEmail.js // ... <span>{currentUser?.email}</span> // ...

In the above code, we are using optional chaining to get the user’s email so that when the email is null our code will throw no errors.

Now, when we refresh the verify-email page, we should see the email of the registered user.

Let’s move to the next thing which is making the Resend Email button work. First, let’s make the necessary imports. Add the following imports to the VerifyEmail.js file:

// src/VerifyEmail.js import {useState} from 'react' import {auth} from './firebase' import {sendEmailVerification} from 'firebase/auth'

Now, let’s add a state that will be responsible for disabling and enabling the Resend Email button based on whether or not the verification email has been sent. This code goes after currentUser in the VerifyEmail component:

// src/VerifyEmail.js const [buttonDisabled, setButtonDisabled] = useState(false)

For the function that handles resending the verification email and disabling/enabling the button, we need this after the buttonDisabled state:

// src/VerifyEmail.js // ...  const resendEmailVerification = () => {   setButtonDisabled(true)   sendEmailVerification(auth.currentUser)   .then(() => {     setButtonDisabled(false)   }).catch((err) => {     alert(err.message)     setButtonDisabled(false)   }) }  // ...

Next, in the return statement, modify the Resend Email button like this:

// ... <button    onClick={resendEmailVerification}   disabled={buttonDisabled}   >Resend Email</button> // ...

Now, if we go over to the verify-email page and click the button, another email will be sent to us. But there is a problem with how we created this functionality because if we try to click the button again in less than a minute, we get an error from Firebase saying we sent too many requests. This is because Firebase has a one minute interval before being able to send another email to the same address. That’s the net thing we need to address.

What we need to do is make the button stay disabled for 60 seconds (or more) after a verification email is sent. We can enhance the user experience a bit by displaying a countdown timer in Resend Email button to let the user know the button is only temporarily disabled.

In the VerifyEmail.js file, add a useEffect import:

import {useState, useEffect} from 'react'

Next, add the following after the buttonDisabled state:

// src/VerifyEmail.js const [time, setTime] = useState(60) const [timeActive, setTimeActive] = useState(false)

In the above code, we have created a time state which will be used for the 60-second countdown and also a timeActive state which will be used to control when the count down will start.

Add the following lines of code after the states we just created:

// src/VerifyEmail.js // ...  useEffect(() => {   let interval = null   if(timeActive && time !== 0 ){     interval = setInterval(() => {       setTime((time) => time - 1)     }, 1000)   }else if(time === 0){     setTimeActive(false)     setTime(60)     clearInterval(interval)   }   return () => clearInterval(interval); }, [timeActive, time])  // ...

In the above code, we created a useEffect hook that only runs when the timeActive or time state changes. In this hook, we are decreasing the previous value of the time state by one every second using the setInterval method, then we are stopping the decrementing of the time state when its value equals zero.

Since the useEffect hook is dependent on the timeActive and time state, one of these states has to change before the time count down can start. Changing the time state is not an option because the countdown has to start only when a verification email has been sent. So, instead, we need to change the timeActive state.

In the resendEmailVerification function, modify the .then method of sendEmailVerification to look like this:

// src/VerifyEmail.js // ... .then(() => {   setButtonDisabled(false)   setTimeActive(true) }) // ...

Now, when an email is sent, the timeActive state will change to true and the count down will start. In the code above we need to change how we are disabling the button because, when the count down is active, we want the disabled button.

We will do that shortly, but right now, let’s make the countdown timer visible to the user. Modify the Resend Email button to look like this:

// src/VerifyEmail.js <button    onClick={resendEmailVerification}   disabled={buttonDisabled} >Resend Email {timeActive && time}</button>

To keep the button in a disabled state while the countdown is active, let’s modify the disabled attribute of the button to look like this:

disabled={timeActive}

With this, the button will be disabled for a minute when a verification email is sent. Now we can go ahead and remove the buttonDisabled state from our code.

Although this functionality works, there is still one problem with how we implemented it: when a user registers and is taken to the verify-email page when they have not received an email yet, they may try to click the Resend Email button, and if they do that in less than a minute, Firebase will error out again because we’ve made too many requests.

To fix this, we need to make the Resend Email button disabled for 60 seconds after an email is sent to the newly registered user. This means we need a way to change the timeActive state within the Register component. We can also use the Context API for this. It will allow us to globally manipulate and access the timeActive state.

Let’s make a few modifications to our code to make things work properly. In the VerifyEmail component, cut the timeActive state and paste it into the App component after the currentUser state.

// src/App.js function App() {   // ...   const [timeActive, setTimeActive] = useState(false)    // ...

Next, put timeActive and setTimeActive inside the object of AuthProvider value prop. It should look like this:

// src/App.js // ... <AuthProvider value={{currentUser, timeActive, setTimeActive}}> // ... 

Now we can access timeActive and setTimeActive within the children of AuthProvider. To fix the error in our code, go to the VerifyEmail.js file and de-structure both timeActive and setTimeActive from useAuthProvider:

// src/VerifyEmail.js const {timeActive, setTimeActive} = useAuthValue()

Now, to change the timeActive state after a verification email has been sent to the registered user, add the following import in the Register.js file:

// src/Register.js import {useAuthValue} from './AuthContext'

Next, de-structure setTimeActive from useAuthValue with this snippet among the other states in the Register component:

// src/Register.js const {setTimeActive} = useAuthValue()

Finally, in the register function, set the timeActive state with the .then the method of sendEmailVerification:

// src/Register.js // ... .then(() => {   setTimeActive(true)   history.push('/verify-email') }) // ...

With this, a user will be able to send a verification email without getting any errors from Firebase.

The last thing to fix concerning user verification is to take the user to their profile page after they have verified their email. To do this, we will use a reload function in the currentUser object. It allows us to reload the user object coming from Firebase, that way we will know when something has changed.

First, let’s make the needed imports. In the VerifyEmail.js file, let’s add this:

// src/VerifyEmail.js import {useHistory} from 'react-router-dom'

We are importing useHistory so that we can use to navigate the user to the profile page. Next, add the following line of code after the states:

// src/VerifyEmail.js const history = useHistory()

And, finally, add the following lines of code after the history variable:

// src/VerifyEmail.js // ...  useEffect(() => {   const interval = setInterval(() => {     currentUser?.reload()     .then(() => {       if(currentUser?.emailVerified){         clearInterval(interval)         history.push('/')       }     })     .catch((err) => {       alert(err.message)     })   }, 1000) }, [history, currentUser])  // ...

In the above code, we are running the reload function every one second until the user’s email has been verified, and, if it has, we are navigating the user to their profile page.

To test this, let’s verify our email by following the instructions in the email sent from Firebase. If all is good, we will be automatically taken to our profile page.

Right now the profile page is showing no user data and he Sign Out link does not work. That’s ur next task.

Working on the user profile page

Let’s start by displaying the Email and Email verified values. For this, we will make use of the currentUser state in AuthContext. What we need to do is import useAuthValue, de-structure currentUser from it, and then display the Email and Email verified value from the user object.

Here is what the Profile.js file should look like:

// src/Profile.js import './profile.css' import {useAuthValue} from './AuthContext'  function Profile() {   const {currentUser} = useAuthValue()    return (     <div className='center'>       <div className='profile'>         <h1>Profile</h1>         <p><strong>Email: </strong>{currentUser?.email}</p>         <p>           <strong>Email verified: </strong>           {`$ {currentUser?.emailVerified}`}         </p>         <span>Sign Out</span>       </div>     </div>   ) }  export default Profile

With this, the Email and Email verified value should now be displayed on our profile page.

To get the sign out functionality working, we will use the signOut function. It takes only one argument, which is the auth instance. So, in Profile.js. let’s add those imports.

// src/Profile.js import { signOut } from 'firebase/auth'  import { auth } from './firebase'

Now, in the return statement, modify the <span> that contains “Sign Out” so that is calls the signOut function when clicked:

// src/Profile.js // ... <span onClick={() => signOut(auth)}>Sign Out</span> // ...

Creating a Private Route for the Profile component

Right now, even with an unverified email address, a user can access the profile page. We don’t want that. Unverified users should be redirected to the login page when they try to access the profile. This is where private routes come in.

In the src directory, let’s create a new PrivateRoute.js file and add the following code to it:

// src/PrivateRoute.js import {Route, Redirect} from 'react-router-dom' import {useAuthValue} from './AuthContext'  export default function PrivateRoute({component:Component, ...rest}) {   const {currentUser} = useAuthValue()    return (     <Route       {...rest}       render={props => {         return currentUser?.emailVerified ? <Component {...props} /> : <Redirect to='/login' />     }}>     </Route>   ) }

This PrivateRoute is almost similar to using the Route. The difference is that we are using a render prop to redirect the user to the profile page if their email is unverified.

We want the profile page to be private, so well import PrivateRoute:

// src/App.js import PrivateRoute from './PrivateRoute'

Then we can replace Route with PrivateRoute in the Profile component. The Profile route should now look like this:

// src/App.js <PrivateRoute exact path="/" component={Profile} />

Nice! We have made the profile page accessible only to users with verified emails.

Creating login functionality

Since only users with verified emails can access their profile page when logged in with the signInWithEmailAndPassword function, we also need to check if their email has been verified and, if it is unverified, the user should be redirected to the verify-email page where the sixty-second countdown should also start.

These are the imports we need to add to the Login.js file:

import {signInWithEmailAndPassword, sendEmailVerification} from 'firebase/auth' import {auth} from './firebase' import {useHistory} from 'react-router-dom' import {useAuthValue} from './AuthContext'

Next, add the following line of code among the states in the Login component.

// src/Login.js const {setTimeActive} = useAuthValue() const history = useHistory()

Then add the following function after the history variable:

// src/Login.js // ...  const login = e => {   e.preventDefault()   signInWithEmailAndPassword(auth, email, password)   .then(() => {     if(!auth.currentUser.emailVerified) {       sendEmailVerification(auth.currentUser)       .then(() => {         setTimeActive(true)         history.push('/verify-email')       })     .catch(err => alert(err.message))   }else{     history.push('/')   }   })   .catch(err => setError(err.message)) }  // ...

This logs in a user and then check if whether they are verified or not. If they are verified, we navigate them to their profile page. But if they are unverified, we send a verification email, then redirect them to the verify-email page.

All we need to do to make this work is call the login function when the form is submitted. So, let’s modify the opening tag of the login_form to this:

// src/Login.js <form onSubmit={login} name='login_form'>

And, hey, we’re done!

Conclusion

In this tutorial, we have learned how to use version 9 of the Firebase Authentication to build a fully functioning user registration and authentication service in React. Is it super easy? No, there are a few thing we need to juggle. But is it a heck of a lot easier than building our own service from scratch? You bet it is! And that’s what I hope you got from reading this.

References


User Registration and Auth Using Firebase and React originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , , , ,

Firebase Crash Course

This article is going to help you, dear front-end developer, understand all that is Firebase. We’re going to cover lots of details about what Firebase is, why it can be useful to you, and show examples of how. But first, I think you’ll enjoy a little story about how Firebase came to be.

Eight years ago, Andrew Lee and James Tamplin were building a real-time chat startup called Envolve. The service was a hit. It was used on sites of celebrities like Ricky Martin and Limp Bizkit. Envolve was for developers who didn’t want to—yet again—build their own chat widget. The value of the service came from the setup ease and speed of message delivery. Envolve was just a chat widget. The process was simple. Place a script tag on a page. Once the widget booted up, it would do everything for you. It was like having a database and server for chat messages already set up.

James and Andrew noticed a peculiar trend as the service became more popular. Some developers would include the widget on the page, but make it invisible. Why would someone want a chat widget with hidden messages? Well, it wasn’t just chat data being sent between devices. It was game data, high scores, app settings, to-dos, or whatever the developer needed to quickly send and synchronize. Developers would listen for new messages in the widget and use them to synchronize state in their apps. This was an easy way to create real-time experiences without the need for a back end.

This was a light bulb moment for the co-founders of Firebase. What if developers could do more than send chat messages? What if they had a service to quickly develop and scale their applications? Remove the responsibility of managing back-end infrastructure. Focus on the front end. This is how Firebase was born.

What is Firebase?

Isn’t Firebase a database? No… yes… mostly no. Firebase is a platform that provides the infrastructure for developers and tools for marketers. But it wasn’t always that way.

Seven years ago, Firebase was a single product: a real-time cloud database. Today, Firebase is a collection of 19 products. Each product is designed to empower a part of an application’s infrastructure. Firebase also gives you insight into how your app is performing, what your users are doing, and how you can make your overall app experience better. While Firebase can make up the entirety of your app’s back-end, you can use each product individually as well.

Here’s just a sampling of those 19 products:

  • Hosting: Deploy a new version of your site for every GitHub pull request.
  • Firestore: Build apps that work in real time, even while offline, with no server required.
  • Auth: Authenticate and manage users with a myriad of providers.
  • Storage: Manage user-generated content like photos, videos, and GIFs.
  • Cloud Functions: Server code driven by events (e.g. record created, user sign up, etc.).
  • Extensions: Pre-packaged functions set up with UI (e.g. Stripe payments, text translations, etc.)
  • Google Analytics: Understand user activity, organized by segments and audiences.
  • Remote Config: Key-value store with dynamic conditions that’s great for feature gating.
  • Performance Monitoring: Page load metrics and custom traces from real usage.
  • Cloud Messaging: Cross-platform push notifications.

Whew. That’s a lot, and I didn’t even list the other nine products. That’s okay. There’s no requirement to use every specific service or even more than one thing. But now it’s time to make these services a little more tangible and showcase what you can do with Firebase.

A great way to learn is by seeing something just work. The first section below will get you set up with Firebase services. The sections following that will highlight the Firebase details of a demo app to showcase how to use Firebase features. While this is a relatively thorough guide to Firebase, it’s not a step-by-step tutorial. The goal is to highlight the working bits in the embedded demos for the sake of covering more ground in this one article. If you want step-by-step Firebase tutorials, leave a comment to hype me up to write one.

A basic Firebase setup

This section is helpful if you plan on forking the demo with your own Firebase back end. You can skip this section if you’re familiar with Firebase Projects or just want to see the shiny demos.

Firebase is a cloud-based service which means you need to do some basic account setup before using its services. Firebase development is not tied to a network connection, however. It’s very much worth noting that you can (and usually should) run Firebase locally on your machine for development. This guide demonstrates building an app with CodePen, which means it needs a cloud-connected service. The goal here is to create your personal back end with Firebase and then retrieve the configuration the front end needs to connect to it.

Create a Firebase Project

Go to the Firebase Console. You’ll be asked if you want to set up Google Analytics. None of these examples use it, so you can skip it and always add it back in later if needed.

Create a web Firebase App

Next, you’ll see options for creating an “App.” Click the web option and give it a name—any name will do. Firebase Projects can hold multiple “apps.” I’m not going to get deep into this hierarchy because it’s not too important when getting started. Once the app is created you’ll be given a configuration object.

let firebaseConfig = {   apiKey: "your-key",   authDomain: "your-domain.firebaseapp.com",   projectId: "your-projectId",   storageBucket: "your-projectId.appspot.com",   messagingSenderId: "your-senderId",   appId: "your-appId",   measurementId: "your-measurementId" };

This is the configuration you’ll use on the front end to connect to Firebase. Don’t worry about any of these properties in terms of security. There’s nothing insecure about including these properties in your front-end code. You’ll learn in one of the sections below how security works in Firebase.

Now it’s time to represent this “App” you created in code. This “app” is merely a container that shares logic and authentication state across different Firebase services. Firebase provides a set of libraries that make development a lot easier. In this example I’ll be using them from a CDN, but they also work well with module bundlers like Webpack and Rollup.

// This pen adds Firebase via the "Add External Scripts" option in codepen // https://www.gstatic.com/firebasejs/8.2.10/firebase-app.js // https://www.gstatic.com/firebasejs/8.2.10/firebase-auth.js  // Create a Project at the Firebase Console // (console.firebase.google.com) let firebaseConfig = {   apiKey: "your-key",   authDomain: "your-domain.firebaseapp.com",   projectId: "your-projectId",   storageBucket: "your-projectId.appspot.com",   messagingSenderId: "your-senderId",   appId: "your-appId",   measurementId: "your-measurementId" };  // Create your Firebase app let firebaseApp = firebase.initializeApp(firebaseConfig); // The auth instance console.log(firebaseApp.auth());

Great! You are so, so close to being able to talk to your very own Firebase back end. Now you need to enable the services you intend to use.

Enable authentication providers

The examples below use authentication to sign in users and secure data in the database. When you create a new Firebase Project, all of your authentication providers are turned off. This is initially inconvenient, but essential for security. You don’t want users trying to sign in with providers your back end does not support.

To turn on a provider go to the Authentication tab in the side navigation and then click the “Sign-in method” button up top. Below, you’ll see a large list of providers such as Email and Password, Google, Facebook, GitHub, Microsoft, and Twitter. For the examples below, you will need to turn on Google and Anonymous. Google is near the top of the list and Anonymous is at the bottom. Selecting Google will ask you to provide a support email, I recommend putting in your own personal email while testing, but production apps should have a dedicated email.

If you plan on using authentication within CodePen, then you’ll also need to add CodePen as an authorized domain. You can add authorized domains towards the bottom of the “Sign-in method” tab.

An important note on this authorization: this will allow any project hosted on cdpn.io to sign into your Firebase Project. There’s not a lot of risk for short-term demo purposes. There’s no cost to using Firebase Auth except for phone number authentication. Ideally, you would not want to keep this as an authorized domain if you plan on using this app in a production capacity.

Now, on to the last step in the Firebase Console: Creating a Firestore database!

Create a Firestore database

Click on Firestore in the left navigation. From here, you’ll need to click the button to create the Firestore database. You’ll be asked if you want to start in “production mode” or “test mode.” You want “test mode” for this example. If you’re worried about security, we’ll cover that in the last section.

Now that you have the basics down, let’s get to some real-life use cases.

Authenticating users

The best parts of an app are usually behind a sign-up form. Why don’t we just let the user in as a guest so they can see for themselves? Systems often require accounts because the back end isn’t designed for guests. There’s a system requirement to have a sacred userId property to save any record belonging to a user. This is where “guest” accounts are helpful. They provide low friction for users to join while giving them a temporary userId that appeases the system. Firebase Auth has a process for doing just this.

Setting up anonymous auth

One of my favorite features about Firebase Auth is anonymous auth. There are two perks. One, you can authenticate a user without taking any input (e.g. passwords, phone number, etc.). Two, you get really good at spelling anonymous. It’s really a win-win situation.

Take this CodePen for example.

It’s a form that lets the user decide if they want to sign in with Google or as a guest. Most of the code in this example is specific to the user interface. The Firebase bits are actually pretty easy.

// Firebase-specific code let firebaseConfig = { /* config */ }; let firebaseApp = firebase.initializeApp(firebaseConfig); // End Firebase-specific code let socialForm = document.querySelector('form.sign-in-social'); let guestForm = document.querySelector('form.sign-in-guest'); guestForm.addEventListener('submit', async submitEvent => {   submitEvent.preventDefault();   let formData = new FormData(guestForm);   let displayName = formData.get('name');   let photoURL = await getRandomPhotoURL();   // Firebase-specific code   let { user } = await firebaseApp.auth().signInAnonymously();   await user.updateProfile({ displayName, photoURL });   // End Firebase-specific code });

In the 17 lines of code above (sans comments), only five of them are Firebase-specific. Four lines are needed to import and configure. Two lines needed to signInAnonymously() and user.updateProfile(). The first sign-in method makes a call to your Firebase back end and authenticates the user. The call returns a result that contains the needed properties, such as the uid. Even with guest users, you can associate data to a user in your back end with this uid. After the user is signed in, the example calls updateProfile on the user object. Even though this user is a guest, they can still have a display name and a profile photo.

Setting up Google auth

The great news is that this works the same exact way with all other permanent providers, like Email and Password, Google, Facebook, Twitter, GitHub, Microsoft, Phone Number, and so much more. Implementing the Google Sign-in only takes a few lines of code as well.

socialForm.addEventListener('submit', submitEvent => {   submitEvent.preventDefault();    // Firebase-specific code   let provider = new firebase.auth.GoogleAuthProvider();   firebaseApp.auth().signInWithRedirect(provider);   // End Firebase-specific code });

Each social style provider initiates a redirect-based authentication flow. That’s a fancy way of saying that the signInWithRedirect method will go to the sign-in page owned by the provider and then return back to your app with the authenticated user. In this case, the user is redirected to Google’s sign-in page, signs in, and then is returned back to your app.

Monitoring authentication state

How do you get the user back from this redirect? There are a few ways, but I’m going to go with the most common. You can detect the authentication state of any active user whether logged in or out.

firebaseApp.auth().onAuthStateChanged(user => {   if(user != null) {     console.log(user.toJSON());   } else {     console.log("No user!");   } });

The onAuthStateChanged method updates whenever there is a change in a user’s authentication state. It will fire initially on page load telling you if a user is logged in, logs in, or logs out. This allows you to build a UI that reacts to these state changes. It also fits well into client-side routers because it can redirect a user to the proper page with a small amount of code.

The app in this case just uses <template> tags to replace the contents of the “phone” element. If the user is logged in, the app routes to the new template.

<div class="container">   <div class="phone">     <!-- Phone contents replaced with template tags -->   </div> </div>

This provides a simple relationship. The .phone is the “root view.” Each <template> tag is a “child view.” The authentication state determines which view is shown.

firebaseApp.auth().onAuthStateChanged(user => {   if(user != null) {     // Show demo view     routeTo("demo", firebaseApp, user);   } else {     console.log("No user!");     // Show log in page     routeTo("signIn", firebaseApp);   } });

The embedded demo isn’t “production ready” as the requirements were just work inside a single CodePen. The goal was to make it easy to read with each “view” contained within a template tag. This loosely emulates what a common routing solution would look like with a framework.

One important bit to note here is that the user object is passed down to the routeTo function. Getting the logged-in state is asynchronous. I find that it’s much easier to pass the user state down for the view than to make async calls from within the view. Many framework routers have a spot for this kind of async data fetching.

Convert guests to permanent users

The <template id="demo"> tag has a submit button for converting guests to permanent users. This is done by having the user sign in with the main provider (again, Google in this case).

let convertForm = document.querySelector('form.convert'); convertForm.addEventListener("submit", submitEvent => {   submitEvent.preventDefault();   let provider = new firebase.auth.GoogleAuthProvider();   firebaseApp.auth().currentUser.linkWithRedirect(provider); });

Using the linkWithRedirect method will kick the user out to authenticate with the social provider. When the redirect returns the account will be merged. There’s nothing you have to change within the onAuthStateChanged method that controls the “child view” routing. What’s important to note is that the uid remains the same.

Handling account merging errors

A few edge cases can pop-up when you merge accounts. In some cases, a user could already have created an account with Google and is now trying to merge that existing account. There are many ways you can handle this scenario depending on how you want your app to work. I’m not going to get deep into this because we have a lot more to cover, but it’s important to know how to handle these errors.

async function checkForRedirect() {   let auth = firebaseApp.auth();   try {     let result = await auth.getRedirectResult();   }   catch (error) {     switch(error.code) {       case 'auth/credential-already-in-use': {         // You can check for the provider(s) in use         let providers = await auth.fetchProvidersForEmail(error.email);         // Then decide what strategy to take. A possible strategy is         // notifying the user and asking them to sign in as that account       }     }   } }

The code above uses the getRedirectResult method to detect if a user has directly returned from a social login redirect. If so, there’s a lot of information in that result. Most importantly here, we want to know if there was a problem. Firebase Auth will throw an error and provide relevant information on the error, such as the email and credentials, which will allow you to continue to merge the account. That’s not always what you want to do. In this case, I would probably indicate to the user that the account exists and prompt them to sign in with it. But I digress; I could talk sign-in forms for ages.

Now it’s on to building the data visualization (okay, it’s just a pie-chart) and providing it with a realtime data stream.

Setting up the data visualization

I’ll be honest. I have no idea what this app really does. I really wanted to test out building pie charts with a conic-gradient. I was excited about using CSS Custom Properties to change the values of the chart and syncing that in real time with a database, like Firestore. Let’s take a brief little detour to discuss how conic-gradient works.

A conic-gradient is a surprisingly well-supported CSS feature. Its key feature is that its color-stops are placed at the circumference of the circle.

.pie-chart {   background-image: conic-gradient(      purple 10%, /* 10% of the circumference */      magenta 0 20%, /* start at 0 go 20%, acts like 10% */      cyan 0 /* Fill the rest */   );   }

You can build a pie chart with some quick math: lastStopPercent + percent. I stored these values in four CSS Custom Properties in the app pen: --pie-{n}-value (replace n with a number). Those values are used in another custom property that serves like a computed function.

:root {   --pie-1-value: 10%;   --pie-2-value: 10%;   --pie-3-value: 80%;    --pie-1-computed: var(--pie-1-value);   --pie-2-computed: 0 calc(var(--pie-1-value) + var(--pie-2-value));   --pie-3-computed: 0 calc(var(--pie-2-value) + var(--pie-3-value)); }

Then the computed values are set in the conic-gradient.

background-image: conic-gradient(   purple var(--pie-1-computed),   magenta var(--pie-2-computed),   cyan 0 );

The last computed value, --pie-3-computed, is ignored since it will always fill to the end (kinda like z for SVG paths). I think it’s still a good idea to set it in JavaScript to make the whole thing feel like it makes sense.

function setPieChartValue(percentage, index) {   let root = document.documentElement;   root.style.setProperty(`--pie-$ {index+1}-value`, `$ {percentage}%`); }  let percentages = [25, 35, 60]; percentages.forEach(setPieChartValue);

You can hook up with pie chart with any data set with this newfound knowledge. The Firestore database has a .onSnapshot() method that streams data back from your database.

const fullPathDoc = firebaseApp.firestore().doc('/users/1234/expenses/3-2021'); fullPathDoc.onSnapshot(snap => {   const { items } = doc.data();   items.forEach(setPieChartValue); });

A real time update will trigger the .onSnapshot() method whenever a value changes in your Firestore database at that document location. Now you might be asking yourself, what is a document location? How do I even store data in this database in the first place? Let’s take a dive into how Firestore works and how to model data in NoSQL databases.

How to model data in NoSQL database

Firestore is a document (NoSQL) database. It provides a hierarchical pattern of collections, which is a list of documents. Think of a document like a JSON object that has many more data types. A document can have a collection itself, which is known as a sub-collection. This is good for structuring data in a “parent-child” or hierarchical pattern.

If you haven’t followed the pre-requisites section up top, give it a read to make sure you have a Firestore database created before using any of this code yourself.

Just like Auth, you first need to import Firestore up top.

// This pen adds Firebase via the "Add External Scripts" option in codepen // https://www.gstatic.com/firebasejs/8.2.10/firebase-app.js // https://www.gstatic.com/firebasejs/8.2.10/firebase-auth.js // https://www.gstatic.com/firebasejs/8.2.10/firebase-firestore.js  let firebaseConfig = { /* config */ }; let firebaseApp = firebase.initializeApp(firebaseConfig);

This tells the Firebase client library how to connect your Firestore database. Using this setup, you can create a reference to a piece of data in your database.

// Reference to collection stored at: '/expenses' const expensesCol = firebaseApp.firestore('expenses');  // Retrieve a snapshot of data (not in realtime) // Top level await is coming! (v8.dev/features/top-level-await) const snapshot = await expensesCol.get(); const expenses = snapshot.docs.map(d => d.data());

The code above creates a “Collection Reference” and then calls the .get() method to retrieve the data snapshot. This is not the data itself; it’s a wrapper that has a lot of helpful methods and metadata about the data. The last line “unwraps” the snapshot by iterating over the .docs array and calling the .data() function for each “Document Snapshot.” Note that this isn’t real time, but that’s coming up in just a bit!

What’s really important to grok here is the data structure (sometimes called a data model). This app stores the “expenses” of a user. Let’s say the document has the following structure.

{   uid: '1234',   items: [     { label: "Food", value: 10 },     { label: "Services", value: 24 },     { label: "Rent", value: 30 },     { label: "Oops", value: 38 }   ] }

The properties of a document are called a field. This document has an array field of items. Each item contains a label string and a value number. There’s also a string field named uid that stores the id of the user who owns the data. This structure simplifies the process of iterating over the values to create the pie chart. That part is solved, but how do we figure out how to get to a specific user’s expenses?

A traditional way of retrieving data based on a constraint is by using a query. That’s something you can do in Firestore with a .where() method.

// Let's pretend currentUser.uid === '1234' const currentUser = firebaseApp.auth().currentUser;  // Reference to collection stored at: '/expenses' const expensesCol = firebaseApp.firestore('expenses');  // Query for the expenses belonging to uid == 1234 const userQuery = expensesCol.where('uid', '==', currentUser.uid); const snapshot = await userQuery.get();

This structure is fine but doesn’t take full advantage of the hierarchy provided by collections and, more importantly, sub-collections. Instead, you can structure your data in a “parent-child” relationship. Structures in Firestore work a lot like URLs for a website. You can design the paths to contain route parameter-like wildcards.

/users/:uid/expenses/month-year

The path above structures the data with a top-level collection of /users, then a document assigned to a uid, followed by a sub-collection. Sub-collections can exist even if there’s no existing parent document. Each sub-collection contains a document where you can retrieve the expenses by putting in the year as a number (e.g. 3 for March) and the year (2021).

// Let's pretend currentUser.uid === '1234' const currentUser = firebaseApp.auth().currentUser;  // Reference to collection stored at: '/users' const usersCol = firebaseApp.firestore().collection('users');  // Reference to document stored at: '/users/1234'; const userDoc = usersCol.doc(currentUser.uid);  // Reference to sub-collection stored at: '/users/1234/expenses' const userExpensesCol = userDoc.collection('expenses');  // Reference to document stored at: '/users/1234/expenses/3-2021'; const marchDoc = userExpensesCol.doc('3-2021');  // Alternatively, you could express a full path: const fullPathDoc = firebaseApp.firestore().doc('/users/1234/expenses/3-2021');

The example above shows that, with a little bit of modeling, you can retrieve a piece of data without needing to write a query. This won’t always be the case for each and every data structure. In many cases, it will be valid to have a top-level collection. It comes down to the kind of queries you want to write and how you want to secure your data. It’s good, however, to know your options when modeling the data. If you want to learn more about this topic, my colleague Todd Kerpelman has recorded a comprehensive series all about Firestore.

We’re going to go with the hierarchical approach in this app. With this data structured let’s stream in real time.

Streaming data to the visualization

The section above detail how to retrieve data in a “one-time” manner with .get(). Both documents and collections also have a .onSnapshot() method that allows you to stream the data in realtime.

const fullPathDoc = firebaseApp.firestore().doc('/users/1234/expenses/3-2021'); fullPathDoc.onSnapshot(snap => {   const { items } = doc.data();   items.forEach((item, index) => {     console.log(item);   }); });

Whenever an update happens for the data stored at fullPathDoc, Firestore will stream the update to all connected clients. Using this data sync you can set all the CSS Custom Properties for the pie chart.

let db = firebaseApp.firestore(); let { uid } = firebaseApp.auth().currentUser; let marchDoc = db.doc(`users/$ {uid}/expenses/3-2021`); marchDoc.onSnapshot(snap => {   let { factors } = snap.data();   factors.forEach((factor, index) => {     root.style.setProperty(`--pie-$ {index+1}__value`, `$ {factor.value}%`);   });     });

Now for the fun part! Go update the data in the database and see the pie slices move around.

Honestly, that is so much fun. I’ve been working on Firebase for nearly seven years and I never get tired of seeing the lightning-fast data updates. But the job is not yet complete! The database is insecure as it can be updated by anyone anywhere. It’s time to make sure it’s secure only to the user who owns that data.

Securing your database

If you’re new to Firebase or back-end development, you might be wondering why the database is insecure at the current moment. Firebase allows you to build apps without running your own server. This means you can connect to a database directly from the browser. So, if it’s that easy to access the database, what keeps someone from coming along and doing something malicious? The answer is Firebase Security Rules.

Security Rules are how you secure access to your Firebase services. You write a set of rules that specify how data is accessed in your back end. Firebase evaluates these rules for each request that comes into your back end and only allows the request if it passes the rule. In other words, you write a set of rules and Firebase runs them on the server to secure access to your services.

Security Rules are like a router

Security Rules are a custom language that works a lot like a router.

rules_version = '2'; service cloud.firestore {   match /databases/{database}/documents {     // When a request comes in for a "user/:userId"     // let's allow the read or write (not very secure)     // Don't copy this in your code plz!     match /users/{userId} {       allow read, write: if userId == "david";     }   } }

The example above starts with a rules_version statement. Don’t worry too much about that, but it’s how you tell Firebase the version of the Security Rules language you want to use. Currently, it’s recommended to use version 2. Then the example goes on to create a service declaration. This tells Firebase what service you are trying to secure, which is Firestore in this case. Now, on to the important part: the match statements.

A match statement is the part that makes rules work like a router. The first match statement in the example establishes that you are matching for documents in the database. It’s a very general statement that says, Hey, look in the documents section. The next match statement is more specific. It looks to match a document within the users collection. The path syntax of /users/{userId} is similar to a routing syntax of /users/:userId where the :userId syntax notes a route parameter. In Security Rules, the {userId} syntax works just like route parameter, except that it’s called “wildcard” here. Any user within the collection can be matched with this statement. What happens when the user is matched? You use the allow statement to control the access.

The allow statement evaluates an expression and, if the result is true, it allows the operation. If it’s false, the operation is rejected. What’s useful about the allow statement is that there’s a lot of useful information to use in the containing match block. One useful piece of information is the wildcard itself. The example above uses the userId wildcard like a variable and tests to see if it matches the value of "``david``". Only a userId of "``david``" will allow the read or write operation.

Now that’s not a very useful rule, but it helps to start simple. Let’s take a moment to remember the structure of the database. Security Rules are a lot like an annotation on top of your data structure. You can make notes at the document paths that enforce your data access. This app stores data at a /users/{userId} collection and expenses at a /users/{userId}/expenses/month-year sub-collection. The security strategy is to ensure that only the authenticated user can read or write their data.

rules_version = '2'; service cloud.firestore {   match /databases/{database}/documents {     match /users/{userId}/{documents=**} {       allow read, write: if request.auth.uid == userId;     }   } }

The example above starts off the same but starts to change when matching the /users/{userId} path. There’s this weird {documents=**} syntax tacked on at the end. This is called the recursive wildcard and it’s a way of cascading a rule to sub-collections—meaning any sub-collection of /users/{userId} will have the same rules applied to it. This is great in the current use case because both /users/{userId} and /users/{userId}/expenses/month-year should follow the same rule.

Inside of that match, the allow statement has been updated with a new variable named request. Security Rules come with an entire set of variables to help you write sophisticated rules. This variable is how you evaluate if the request comes from an authenticated user. The allow statement evaluates if the authenticated user has a uid that matches the {userId} wildcard. If that statement evaluates to true, the read or write is allowed. If the user is not authenticated or does not match the {userId} wildcard, no operation is allowed. Therefore, it’s secure!

The following snippet shows two requests from an authenticated user.

// Let's pretend currentUser.uid === '1234' const currentUser = firebaseApp.auth().currentUser;  // The authenticated user owns this sub-collection const ownedDoc = firebaseApp.firestore().doc('/users/1234/expenses/3-2021');  // The authenticated user DOES NOT own this sub-collection  const notOwnedDoc = firebaseApp.firestore().doc('/users/abcxyz/expenses/3-2021');  try {   const ownedSnapshot = await ownedDoc.get();   const notOwnedSnapshot = await notOwnedDoc.get(); } catch (error) {   // This will result in an error because the `notOwnedDoc` request will fail the security rule }

Just like that, you have secured a collection and a sub-collection. But what about the rest of the data in the database? What happens if you don’t write any rules for data at those paths? The example above will only allow access that matches the allow statement for the /users collection and its sub-collections. No other reads or writes will work, at all, for any other collections or sub-collections. In other words, if you don’t write an allow statement for a path, then it won’t allow any reads or writes. This is a great default for security because it makes you explicitly enforce your access at the appropriate paths. You can mess this up, however!

rules_version = '2'; service cloud.firestore {   match /databases/{database}/documents {     // Ahh!!! Never do this in a production app!!!     // This will negate any rule you write below!!     // Don't copy and paste this into your rules!!     match /{document=**} {       allow read, write: if true;     }     match /users/{userId}/{documents=**} {       allow read, write: if request.auth.uid == userId;     }   } }

The sample above adds a match block that matches the path of /{document=**}. This is a recursive wildcard path that matches every document in the entire database. The allow statement always evaluates to true because it is true. This match block creates an overlapping match statement, meaning two or more match blocks that match on the same path. This isn’t like CSS where the last rule wins. Security Rules will evaluate each rule matched and if any of them evaluate to true, the operation is allowed. Therefore, the global recursive wildcard will negate any secure rule you have below. The global recursive wildcard is a strategy for opening up your database for a short-term “test mode” where there is no important non-publicly accessible data (nothing private or important) saved. Outside of that, I don’t recommend using it.

Write rules locally or in the console

The final topic to touch upon is where you write and save your rules. You have two options. The first is inside of the Firebase Console. Within the Firestore data viewer you’ll see an option for “Rules.” This tab shows you the rules that are active for your database. From here, you can write and even test scenarios against your rules. This approach is recommended for those who are getting started and trying to become familiar with Security Rules.

Another option is write rules locally on your machine and use the Firebase CLI to deploy them to the console. This allows you to keep them in source control and write tests for them to make sure they continue to work as your codebase evolves. This is the recommended approach for production apps and teams.

It’s worth noting again that your Firebase configuration used to create a Firebase App is not insecure. It’s the equivalent of someone knowing the domain name of your site. Someone knowing your domain name doesn’t make your site insecure. Security Rules are Firebase’s way of providing secure access to your data and your services.

Wrapping things up

That was a lot of Firebase information, especially about all the stuff about rules (security is important!). The information in this article signs in users, merges guest accounts, structures data, streams it to a visualization in real time, and makes sure it’s all secure. You can use these concepts to build so many different applications.

There are so many things I want you to know if you want to continue building with Firebase, such as the Emulator Suite. The app built in this article can run locally on your machine, which is a far easier development experience and great for testing in CI/CD environments. There are also a lot of great Firebase tools and framework libraries. Here are some links worth checking out.

If there’s one thing I hope you saw in this article, it’s that there aren’t too many lines of front-end Firebase code. A line here to sign in a user, a few lines there to get the data, but mostly code that’s specific to the app itself. The goal of Firebase is to allow you to build quickly. Remove the responsibility of managing backend infrastructure. Focus on the front end.


The post Firebase Crash Course appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Building a Real-Time Chat App with React and Firebase

In this article, we’ll cover key concepts for authenticating a user with Firebase in a real-time chat application. We’ll integrate third-party auth providers (e.g. Google, Twitter and GitHub) and, once users are signed in, we’ll learn how to store user chat data in the Firebase Realtime Database, where we can sync data with a NoSQL cloud database.

The client application is going to be built in React, as it is one of the most popular JavaScript frameworks out there, but the concepts can also be applied to other frameworks.

But first, what is Firebase?

Firebase is Google’s mobile platform for quickly developing apps. Firebase provides a suite of tools for authenticating applications, building reactive client apps, reporting analytics, as well as a host of other helpful resources for managing apps in general. It also provides back-end management for web, iOS, Android, and Unity, a 3D development platform.

Out of the box, Firebase is packaged with features that help developers like ourselves focus on building apps while it handles all server-side logic. Things like:

  • Authentication: This includes support for email and password authentication as well as single sign-on capabilities (via Facebook, Twitter and Google).
  • Realtime database: This is a “NoSQL” database that updates in real time.
  • Cloud functions: These run extra server-side logic.
  • Static hosting: This is a means of serving assets pre-built instead of rendering at runtime.
  • Cloud storage: This gives us a place to store media assets.

Firebase offers a generous free tier that includes authentication and access to their Realtime Database. The authentication providers we’ll be covering email and password — Google and GitHub — are free on that side as well. The Realtime Database allows up to 100 simultaneous connections and 1 gigabyte storage per month. A full table of pricing can be found on the Firebase website.

Here’s what we’re making

We’re going to build an application called Chatty. It will allow only authenticated users to send and read messages and users can sign up by providing their email and creating a password, or by authenticating through a Google or GitHub account. Check out source code if you want to refer to it or take a peek as we get started.

We’ll end up with something like this:

Setting up

You’re going to need a Google account to use Firebase, so snag one if you haven’t already. And once you do, we can officially kick the tires on this thing.

First off, head over to the Firebase Console and click the “Add project” option.

Next, let’s enter a name for the project. I’m going with Chatty.

You can choose to add analytics to your project, but it’s not required. Either way, click continue to proceed and Firebase will take a few seconds to delegate resources for the project.

Once that spins up, we are taken to the Firebase dashboard But, before we can start using Firebase in our web app, we have to get the configuration details down for our project. So, click on the web icon in the dashboard.

Then, enter a name for the app and click Register app.

Next up, we’ll copy and store the configuration details on the next screen in a safe place. That will come in handy in the next step.

Again, we’re going to authenticate users via email and password, with additional options for single sign-on with a Google or GitHub account. We need to enable these from the Authentication tab in the dashboard, but we’ll go through each of them one at a time.

Email and password authentication

There’s a Sign-in method tab in the Firebase dashboard. Click the Email/Password option and enable it.

Now we can use it in our app!

Setting up the web app

For our web app, we’ll be using React but most of the concepts can be applied to any other framework. Well need Node.js for a React setup, so download and install it if you haven’t already.

We’ll use create-react-app to bootstrap a new React project. This downloads and installs the necessary packages required for a React application. In the terminal, cd into where you’d like our Chatty project to go and run this to initialize it:

npx create-react-app chatty

This command does the initial setup for our react app and installs the dependencies in package.json. We’ll also install some additional packages. So, let’s cd into the project itself and add packages for React Router and Firebase.

cd chatty yarn add react-router-dom firebase

We already know why we need Firebase, but why React Router? Our chat app will have a couple of views we can use React Router to handle navigating between pages.

With that done, we can officially start the app:

yarn start

This starts a development server and opens a URL in your default browser. If everything got installed correctly, you should see a screen like this:

Looking at the folder structure, you would see something similar to this:

For our chat app, this is the folder structure we’ll be using:

  • /components: contains reusable widgets used in different pages
  • /helpers: a set of reusable functions
  • /pages: the app views
  • /services: third-party services that we’re using (e.g. Firebase)
  • App.js: the root component

Anything else in the folder is unnecessary for this project and can safely be removed. From here, let’s add some code to src/services/firebase.js so the app can talk with Firebase.

import firebase from 'firebase';

Let’s get Firebase into the app

We’ll import and initialize Firebase using the configuration details we copied earlier when registering the app in the Firebase dashboard. Then, we’ll export the authentication and database modules.

const config = {   apiKey: "ADD-YOUR-DETAILS-HERE",   authDomain: "ADD-YOUR-DETAILS-HERE",   databaseURL: "ADD-YOUR-DETAILS-HERE" }; firebase.initializeApp(config); export const auth = firebase.auth; export const db = firebase.database();

Let’s import our dependencies in src/App.js:

import React, { Component } from 'react'; import {   Route,   BrowserRouter as Router,   Switch,   Redirect, } from "react-router-dom"; import Home from './pages/Home'; import Chat from './pages/Chat'; import Signup from './pages/Signup'; import Login from './pages/Login'; import { auth } from './services/firebase';

These are ES6 imports. Specifically, we’re importing React and other packages needed to build out the app. We’re also importing all the pages of our app that we’ll configure later to our router.

Next up is routing

Our app has public routes (accessible without authentication) and a private route (accessible only with authentication). Because React doesn’t provide a way to check the authenticated state, we’ll create higher-order components (HOCs) for both types of routes.

Our HOCs will:

  • wrap a <Route>,
  • pass props from the router to the <Route>,
  • render the component depending on the authenticated state, and
  • redirect the user to a specified route if the condition is not met

Let’s write the code for our <PrivateRoute> HOC.

function PrivateRoute({ component: Component, authenticated, ...rest }) {   return (     <Route       {...rest}       render={(props) => authenticated === true         ? <Component {...props} />         : <Redirect to={{ pathname: '/login', state: { from: props.location } }} />}     />   ) }

It receives three props: the component to render if the condition is true, the authenticated state, and the ES6 spread operator to get the remaining parameters passed from the router. It checks if authenticated is true and renders the component passed, else it redirects to/login.

function PublicRoute({ component: Component, authenticated, ...rest }) {   return (     <Route       {...rest}       render={(props) => authenticated === false         ? <Component {...props} />         : <Redirect to='/chat' />}     />   ) }

The <PublicRoute> is pretty much the same. It renders our public routes and redirects to the /chat path if the authenticated state becomes true. We can use the HOCs in our render method:

render() {   return this.state.loading === true ? <h2>Loading...</h2> : (     <Router>       <Switch>         <Route exact path="/" component={Home}></Route>         <PrivateRoute path="/chat" authenticated={this.state.authenticated} component={Chat}></PrivateRoute>         <PublicRoute path="/signup" authenticated={this.state.authenticated} component={Signup}></PublicRoute>         <PublicRoute path="/login" authenticated={this.state.authenticated} component={Login}></PublicRoute>       </Switch>     </Router>   ); }

Checking for authentication

It would be nice to show a loading indicator while we verify if the user is authenticated. Once the check is complete, we render the appropriate route that matches the URL. We have three public routes — <Home>, <Login> and <Signup> — and a private one called <Chat>.

Let’s write the logic to check if the user is indeed authenticated.

class App extends Component {   constructor() {     super();     this.state = {       authenticated: false,       loading: true,     };   } }  export default App;

Here we’re setting the initial state of the app. Then, we’re using the componentDidMount lifecycle hook to check if the user is authenticated. So, let’s add this after the constructor:

componentDidMount() {   this.removelistener = auth().onAuthStateChanged((user) => {     if (user) {       this.setState({         authenticated: true,         loading: false,       });     } else {       this.setState({         authenticated: false,         loading: false,       });     }   }) }

Firebase provides an intuitive method called onAuthStateChanged that is triggered when the authenticated state changes. We use this to update our initial state. user is null if the user is not authenticated. If the user is true, we update authenticated to true; else we set it to false. We also set loading to false either way.

Registering users with email and password

Users will be able to register for Chatty through email and password. The helpers folder contains a set of methods that we’ll use to handle some authentication logic. Inside this folder, let’s create a new file called auth.js and add this:

import { auth } from "../services/firebase";

We import the auth module from the service we created earlier.

export function signup(email, password) {   return auth().createUserWithEmailAndPassword(email, password); } 
 export function signin(email, password) {   return auth().signInWithEmailAndPassword(email, password); }

We have two methods here: signup andsignin:

  • signup will create a new user using their email and password. 
  • signin will log in an existing user created with email and password.

Let’s create our <Signup> page by creating a new file Signup.js file in the pages folder. This is the markup for the UI:

import React, { Component } from 'react'; import { Link } from 'react-router-dom'; import { signup } from '../helpers/auth'; 
 export default class SignUp extends Component { 
   render() {     return (       <div>         <form onSubmit={this.handleSubmit}>           <h1>             Sign Up to           <Link to="/">Chatty</Link>           </h1>           <p>Fill in the form below to create an account.</p>           <div>             <input placeholder="Email" name="email" type="email" onChange={this.handleChange} value={this.state.email}></input>           </div>           <div>             <input placeholder="Password" name="password" onChange={this.handleChange} value={this.state.password} type="password"></input>           </div>           <div>             {this.state.error ? <p>{this.state.error}</p> : null}             <button type="submit">Sign up</button>           </div>           <hr></hr>           <p>Already have an account? <Link to="/login">Login</Link></p>         </form>       </div>     )   } }
Email? Check. Password? Check. Submit button? Check. Our form is looking good.

The form and input fields are bound to a method we haven’t created yet, so let’s sort that out. Just before the render() method, we’ll add the following:

constructor(props) {   super(props);   this.state = {     error: null,     email: '',     password: '',   };   this.handleChange = this.handleChange.bind(this);   this.handleSubmit = this.handleSubmit.bind(this); }

We’re setting the initial state of the page. We’re also binding the handleChange and handleSubmit methods to the component’s this scope.

handleChange(event) {   this.setState({     [event.target.name]: event.target.value   }); }

Next up, we’ll add the handleChange method that our input fields are bound to. The method uses computed properties to dynamically determine the key and set the corresponding state variable.

async handleSubmit(event) {   event.preventDefault();   this.setState({ error: '' });   try {     await signup(this.state.email, this.state.password);   } catch (error) {     this.setState({ error: error.message });   } }

For handleSubmit, we’re preventing the default behavior for form submissions (which simply reloads the browser, among other things). We’re also clearing up the error state variable, then using the signup() method imported from helpers/auth to pass the email and password entered by the user.

If the registration is successful, users get redirected to the /Chats route. This is possible with the combination of onAuthStateChanged and the HOCs we created earlier. If registration fails, we set the error variable which displays a message to users.

Authenticating users with email and password

The login page is identical to the signup page. The only difference is we’ll be using the signin method from the helpers we created earlier. That said, let’s create yet another new file in the pages directory, this time called Login.js, with this code in it:

import React, { Component } from "react"; import { Link } from "react-router-dom"; import { signin, signInWithGoogle, signInWithGitHub } from "../helpers/auth"; 
 export default class Login extends Component {   constructor(props) {     super(props);     this.state = {       error: null,       email: "",       password: ""     };     this.handleChange = this.handleChange.bind(this);     this.handleSubmit = this.handleSubmit.bind(this);   } 
   handleChange(event) {     this.setState({       [event.target.name]: event.target.value     });   } 
   async handleSubmit(event) {     event.preventDefault();     this.setState({ error: "" });     try {       await signin(this.state.email, this.state.password);     } catch (error) {       this.setState({ error: error.message });     }   } 
   render() {     return (       <div>         <form           autoComplete="off"           onSubmit={this.handleSubmit}         >           <h1>             Login to             <Link to="/">               Chatty             </Link>           </h1>           <p>             Fill in the form below to login to your account.           </p>           <div>             <input               placeholder="Email"               name="email"               type="email"               onChange={this.handleChange}               value={this.state.email}             />           </div>           <div>             <input               placeholder="Password"               name="password"               onChange={this.handleChange}               value={this.state.password}               type="password"             />           </div>           <div>             {this.state.error ? (               <p>{this.state.error}</p>             ) : null}             <button type="submit">Login</button>           </div>           <hr />           <p>             Don't have an account? <Link to="/signup">Sign up</Link>           </p>         </form>       </div>     );   } }

Again, very similar to before. When the user successfully logs in, they’re redirected to /chat.

Authenticating with a Google account

Firebase allows us to authenticate users with a valid Google account. We’ve got to enable it in the Firebase dashboard just like we did for email and password.

Select the Google option and enable it in the settings.

On that same page, we also need to scroll down to add a domain to the list of domains that are authorized to access feature. This way, we avoid spam from any domain that is not whitelisted. For development purposes, our domain is localhost, so we’ll go with that for now.

We can switch back to our editor now. We’ll add a new method to helpers/auth.js to handle Google authentication.

export function signInWithGoogle() {   const provider = new auth.GoogleAuthProvider();   return auth().signInWithPopup(provider); }

Here, we’re creating an instance of the GoogleAuthProvider. Then, we’re calling signInWithPopup with the provider as a parameter. When this method is called, a pop up will appear and take the user through the Google sign in flow before redirecting them back to the app. You’ve likely experienced it yourself at some point in time.

Let’s use it in our signup page by importing the method:

import { signin, signInWithGoogle } from "../helpers/auth";

Then, let’s add a button to trigger the method, just under the Sign up button:

<p>Or</p> <button onClick={this.googleSignIn} type="button">   Sign up with Google </button>

Next, we’ll add the onClick handler:

async googleSignIn() {   try {     await signInWithGoogle();   } catch (error) {     this.setState({ error: error.message });   } }

Oh, and we should remember to bind the handler to the component:

constructor() {   // ...   this.githubSignIn = this.githubSignIn.bind(this); }

That’s all we need! When the button is clicked, it takes users through the Google sign in flow and, if successful, the app redirects the user to the chat route.

Authenticating with a GitHub account

We’re going to do the same thing with GitHub. May as well give folks more than one choice of account.

Let’s walk through the steps. First, we’ll enable GitHub sign in on Firebase dashboard, like we did for email and Google.

You will notice both the client ID and client secret fields are empty, but we do have our authorization callback URL at the bottom. Copy that, because we’ll use it when we do our next thing, which is register the app on GitHub.

Once that’s done, we’ll get a client ID and secret which we can now add to the Firebase console.

Let’s switch back to the editor and add a new method to helpers/auth.js:

export function signInWithGitHub() {   const provider = new auth.GithubAuthProvider();   return auth().signInWithPopup(provider); }

It’s similar to the Google sign in interface, but this time we’re creating a GithubAuthProvider. Then, we’ll call signInWithPopup with the provider.

In pages/Signup.js, we update our imports to include the signInWithGitHub method:

import { signup, signInWithGoogle, signInWithGitHub } from "../helpers/auth";

We add a button for GitHub sign up:

<button type="button" onClick={this.githubSignIn}>   Sign up with GitHub </button>

Then we add a click handler for the button which triggers the GitHub sign up flow:

async githubSignIn() {   try {     await signInWithGitHub();   } catch (error) {     this.setState({ error: error.message });   } }

Let’s remember again to bind the handler to the component:

constructor() {   // ...   this.githubSignIn = this.githubSignIn.bind(this); }

Now we’ll get the same sign-in and authentication flow that we have with Google, but with GitHub.

Reading data from Firebase

Firebase has two types of databases: A product they call Realtime Database and another called Cloud Firestore. Both databases are NoSQL-like databases, meaning the database is structured as key-value pairs. For this tutorial, we’ll use the Realtime Database.

This is the structure we’ll be using for our app. We have a root node chats with children nodes. Each child has a content, timestamp, and user ID. One of the tabs you’ll notice is Rules which is how we set permissions on the contents of the database.

Firebase database rules are defined as key-value pairs as well. Here, we’ll set our rules to allow only authenticated users to read and write to the chat node. There are a lot more firebase rules. worth checking out.

Let’s write code to read from the database. First, create a new file called Chat.js  in the pages  folder and add this code to import React, Firebase authentication, and Realtime Database:

import React, { Component } from "react"; import { auth } from "../services/firebase"; import { db } from "../services/firebase"

Next, let’s define the initial state of the app:

export default class Chat extends Component {   constructor(props) {     super(props);     this.state = {       user: auth().currentUser,       chats: [],       content: '',       readError: null,       writeError: null     };   }   async componentDidMount() {     this.setState({ readError: null });     try {       db.ref("chats").on("value", snapshot => {         let chats = [];         snapshot.forEach((snap) => {           chats.push(snap.val());         });         this.setState({ chats });       });     } catch (error) {       this.setState({ readError: error.message });     }   } }

The real main logic takes place in componentDidMount. db.ref("chats") is a reference to the chats path in the database. We listen to the value event which is triggered anytime a new value is added to the chats node. What is returned from the database is an array-like object that we loop through and push each object into an array. Then, we set the chats state variable to our resulting array. If there is an error, we set the readError state variable to the error message.

One thing to note here is that a connection is created between the client and our Firebase database because we used the .on() method. This means any time a new value is added to the database, the client app is updated in real-time which means users can see new chats without a page refresh Nice!.

After componentDidMount, we can render our chats like so:

render() {   return (     <div>       <div className="chats">         {this.state.chats.map(chat => {           return <p key={chat.timestamp}>{chat.content}</p>         })}       </div>       <div>         Login in as: <strong>{this.state.user.email}</strong>       </div>     </div>   ); }

This renders the array of chats. We render the email of the currently logged in user.

Writing data to Firebase

At the moment, users can only read from the database but are unable to send messages. What we need is a form with an input field that accepts a message and a button to send the message to the chat.

So, let’s modify the markup like so:

return (     <div>       <div className="chats">         {this.state.chats.map(chat => {           return <p key={chat.timestamp}>{chat.content}</p>         })}       </div>       {# message form #}       <form onSubmit={this.handleSubmit}>         <input onChange={this.handleChange} value={this.state.content}></input>         {this.state.error ? <p>{this.state.writeError}</p> : null}         <button type="submit">Send</button>       </form>       <div>         Login in as: <strong>{this.state.user.email}</strong>       </div>     </div>   ); }

We have added a form with an input field and a button. The value of the input field is bound to our state variable content and we call handleChange when its value changes.

handleChange(event) {   this.setState({     content: event.target.value   }); }

handleChange gets the value from the input field and sets on our state variable. To submit the form, we call handleSubmit:

async handleSubmit(event) {   event.preventDefault();   this.setState({ writeError: null });   try {     await db.ref("chats").push({       content: this.state.content,       timestamp: Date.now(),       uid: this.state.user.uid     });     this.setState({ content: '' });   } catch (error) {     this.setState({ writeError: error.message });   } }

We set any previous errors to null. We create a reference to the chats node in the database and use push() to create a unique key and pushe the object to it.

As always, we have to bind our methods to the component:

constructor(props) {   // ...   this.handleChange = this.handleChange.bind(this);   this.handleSubmit = this.handleSubmit.bind(this); }

Now a user can add new messages to the chats and see them in real-time! How cool is that?

Demo time!

Enjoy your new chat app!

Congratulations! You have just built a chat tool that authenticates users with email and password, long with options to authenticate through a Google or GitHub account.

I hope this give you a good idea of how handy Firebase can be to get up and running with authentication on an app. We worked on a chat app, but the real gem is the signup and sign-in methods we created to get into it. That’s something useful for many apps.

Questions? Thoughts? Feedback? Let me know in the comments!

The post Building a Real-Time Chat App with React and Firebase appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Six Months Using Firebase Web Performance Monitoring

I don’t really think of Firebase as a performance monitoring tool (all I ever think about is auth and real-time data storage), but nevertheless, it totally has that feature.

Justin Ribeiro

[A] tool to track what real users in the wild are experiencing with an easy setup? Yes, please. […] I’ve been using Firebase web perf tracking since June on this very blog. Let’s take a look at the good, the bad, and the downright confusing portions of the Firebase web performance monitoring.

Justin talks about the good and bad of this particular product, but what I think is notable about this kind of performance tooling is that it is reflective of real users using your production site. A lot of performance tooling tests is just fancied up WebPageTest that runs your site once on probably-simulated browser conditions. I don’t see as much happening in the real user performance monitoring space.

I think I’d rank performance testing by type like this:

  1. Run simulated performance metrics in CI. Stop merge requests that break metrics/budgets.
  2. Measure real user monitoring in production.
  3. Run simulated performance metrics in production.

Direct Link to ArticlePermalink

The post Six Months Using Firebase Web Performance Monitoring appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Build a 100% Serverless REST API with Firebase Functions & FaunaDB

Indie and enterprise web developers alike are pushing toward a serverless architecture for modern applications. Serverless architectures typically scale well, avoid the need for server provisioning and most importantly are easy and cheap to set up! And that’s why I believe the next evolution for cloud is serverless because it enables developers to focus on writing applications.

With that in mind, let’s build a REST API (because will we ever stop making these?) using 100% serverless technology.

We’re going to do that with Firebase Cloud Functions and FaunaDB, a globally distributed serverless database with native GraphQL.

Those familiar with Firebase know that Google’s serverless app-building tools also provide multiple data storage options: Firebase Realtime Database and Cloud Firestore. Both are valid alternatives to FaunaDB and are effectively serverless.

But why choose FaunaDB when Firestore offers a similar promise and is available with Google’s toolkit? Since our application is quite simple, it does not matter that much. The main difference is that once my application grows and I add multiple collections, then FaunaDB still offers consistency over multiple collections whereas Firestore does not. In this case, I made my choice based on a few other nifty benefits of FaunaDB, which you will discover as you read along — and FaunaDB’s generous free tier doesn’t hurt, either. 😉

In this post, we’ll cover:

  • Installing Firebase CLI tools
  • Creating a Firebase project with Hosting and Cloud Function capabilities
  • Routing URLs to Cloud Functions
  • Building three REST API calls with Express
  • Establishing a FaunaDB Collection to track your (my) favorite video games
  • Creating FaunaDB Documents, accessing them with FaunaDB’s JavaScript client API, and performing basic and intermediate-level queries
  • And more, of course!

Set Up A Local Firebase Functions Project

For this step, you’ll need Node v8 or higher. Install firebase-tools globally on your machine:

$  npm i -g firebase-tools

Then log into Firebase with this command:

$  firebase login

Make a new directory for your project, e.g. mkdir serverless-rest-api and navigate inside.

Create a Firebase project in your new directory by executing firebase login.

Select Functions and Hosting when prompted.

Choose “functions” and “hosting” when the bubbles appear, create a brand new firebase project, select JavaScript as your language, and choose yes (y) for the remaining options.

Create a new project, then choose JavaScript as your Cloud Function language.

Once complete, enter the functions directory, this is where your code lives and where you’ll add a few NPM packages.

Your API requires Express, CORS, and FaunaDB. Install it all with the following:

$  npm i cors express faunadb

Set Up FaunaDB with NodeJS and Firebase Cloud Functions

Before you can use FaunaDB, you need to sign up for an account.

When you’re signed in, go to your FaunaDB console and create your first database, name it “Games.”

You’ll notice that you can create databases inside other databases . So you could make a database for development, one for production or even make one small database per unit test suite. For now we only need ‘Games’ though, so let’s continue.

Create a new database and name it “Games.”

Then tab over to Collections and create your first Collection named ‘games’. Collections will contain your documents (games in this case) and are the equivalent of a table in other databases— don’t worry about payment details, Fauna has a generous free-tier, the reads and writes you perform in this tutorial will definitely not go over that free-tier. At all times you can monitor your usage in the FaunaDB console.

For the purpose of this API, make sure to name your collection ‘games’ because we’re going to be tracking your (my) favorite video games with this nerdy little API.

Create a Collection in your Games Database and name it “Games.”

Tab over to Security, and create a new Key and name it “Personal Key.” There are 3 different types of keys, Admin/Server/Client. Admin key is meant to manage multiple databases, A Server key is typically what you use in a backend which allows you to manage one database. Finally a client key is meant for untrusted clients such as your browser. Since we’ll be using this key to access one FaunaDB database in a serverless backend environment, choose ‘Server key’.

Under the Security tab, create a new Key. Name it Personal Key.

Save the key somewhere, you’ll need it shortly.

Build an Express REST API with Firebase Functions

Firebase Functions can respond directly to external HTTPS requests, and the functions pass standard Node Request and Response objects to your code — sweet. This makes Google’s Cloud Function requests accessible to middleware such as Express.

Open index.js inside your functions directory, clear out the pre-filled code, and add the following to enable Firebase Functions:

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

Import the FaunaDB library and set it up with the secret you generated in the previous step:

admin.initializeApp(...)   const faunadb = require('faunadb') const q = faunadb.query const client = new faunadb.Client({   secret: 'secrety-secret...that’s secret :)' })

Then create a basic Express app and enable CORS to support cross-origin requests:

const client = new faunadb.Client({...})   const express = require('express') const cors = require('cors') const api = express()   // Automatically allow cross-origin requests api.use(cors({ origin: true }))

You’re ready to create your first Firebase Cloud Function, and it’s as simple as adding this export:

api.use(cors({...}))   exports.api = functions.https.onRequest(api)

This creates a cloud function named, “api” and passes all requests directly to your api express server.

Routing an API URL to a Firebase HTTPS Cloud Function

If you deployed right now, your function’s public URL would be something like this: https://project-name.firebaseapp.com/api. That’s a clunky name for an access point if I do say so myself (and I did because I wrote this… who came up with this useless phrase?)

To remedy this predicament, you will use Firebase’s Hosting options to re-route URL globs to your new function.

Open firebase.json and add the following section immediately below the “ignore” array:

"ignore": [...], "rewrites": [   {     "source": "/api/v1**/**",     "function": "api"   } ]

This setting assigns all /api/v1/... requests to your brand new function, making it reachable from a domain that humans won’t mind typing into their text editors.

With that, you’re ready to test your API. Your API that does… nothing!

Respond to API Requests with Express and Firebase Functions

Before you run your function locally, let’s give your API something to do.

Add this simple route to your index.js file right above your export statement:

api.get(['/api/v1', '/api/v1/'], (req, res) => {   res     .status(200)     .send(`<img src="https://media.giphy.com/media/hhkflHMiOKqI/source.gif">`) })   exports.api = ...

Save your index.js fil, open up your command line, and change into the functions directory.

If you installed Firebase globally, you can run your project by entering the following: firebase serve.

This command runs both the hosting and function environments from your machine.

If Firebase is installed locally in your project directory instead, open package.json and remove the --only functions parameter from your serve command, then run npm run serve from your command line.

Visit localhost:5000/api/v1/ in your browser. If everything was set up just right, you will be greeted by a gif from one of my favorite movies.

And if it’s not one of your favorite movies too, I won’t take it personally but I will say there are other tutorials you could be reading, Bethany.

Now you can leave the hosting and functions emulator running. They will automatically update as you edit your index.js file. Neat, huh?

FaunaDB Indexing

To query data in your games collection, FaunaDB requires an Index.

Indexes generally optimize query performance across all kinds of databases, but in FaunaDB, they are mandatory and you must create them ahead of time.

As a developer just starting out with FaunaDB, this requirement felt like a digital roadblock.

“Why can’t I just query data?” I grimaced as the right side of my mouth tried to meet my eyebrow.

I had to read the documentation and become familiar with how Indexes and the Fauna Query Language (FQL) actually work; whereas Cloud Firestore creates Indexes automatically and gives me stupid-simple ways to access my data. What gives?

Typical databases just let you do what you want and if you do not stop and think: : “is this performant?” or “how much reads will this cost me?” you might have a problem in the long run. Fauna prevents this by requiring an index whenever you query.
As I created complex queries with FQL, I began to appreciate the level of understanding I had when I executed them. Whereas Firestore just gives you free candy and hopes you never ask where it came from as it abstracts away all concerns (such as performance, and more importantly: costs).

Basically, FaunaDB has the flexibility of a NoSQL database coupled with the performance attenuation one expects from a relational SQL database.

We’ll see more examples of how and why in a moment.

Adding Documents to a FaunaDB Collection

Open your FaunaDB dashboard and navigate to your games collection.

In here, click NEW DOCUMENT and add the following BioShock titles to your collection:

{   "title": "BioShock",   "consoles": [     "windows",     "xbox_360",     "playstation_3",     "os_x",     "ios",     "playstation_4",     "xbox_one"   ],   "release_date": Date("2007-08-21"),   "metacritic_score": 96 }  {   "title": "BioShock 2",   "consoles": [     "windows",     "playstation_3",     "xbox_360",     "os_x"   ],   "release_date": Date("2010-02-09"),   "metacritic_score": 88 }{   "title": "BioShock Infinite",   "consoles": [     "windows",     "playstation_3",     "xbox_360",     "os_x",     "linux"   ],   "release_date": Date("2013-03-26"),   "metacritic_score": 94 }

As with other NoSQL databases, the documents are JSON-style text blocks with the exception of a few Fauna-specific objects (such as Date used in the “release_date” field).

Now switch to the Shell area and clear your query. Paste the following:

Map(Paginate(Match(Index("all_games"))),Lambda("ref",Var("ref")))

And click the “Run Query” button. You should see a list of three items: references to the documents you created a moment ago.

In the Shell, clear out the query field, paste the query provided, and click “Run Query.”

It’s a little long in the tooth, but here’s what the query is doing.

Index("all_games") creates a reference to the all_games index which Fauna generated automatically for you when you established your collection.These default indexes are organized by reference and return references as values. So in this case we use the Match function on the index to return a Set of references. Since we do not filter anywhere, we will receive every document in the ‘games’ collection.

The set that was returned from Match is then passed to Paginate. This function as you would expect adds pagination functionality (forward, backward, skip ahead). Lastly, you pass the result of Paginate to Map, which much like its software counterpart lets you perform an operation on each element in a Set and return an array, in this case it is simply returning ref (the reference id).

As we mentioned before, the default index only returns references. The Lambda operation that we fed to Map, pulls this ref field from each entry in the paginated set. The result is an array of references.

Now that you have a list of references, you can retrieve the data behind the reference by using another function: Get.

Wrap Var("ref") with a Get call and re-run your query, which should look like this:

Map(Paginate(Match(Index("all_games"))),Lambda("ref",Get(Var("ref"))))

Instead of a reference array, you now see the contents of each video game document.

Wrap Var("ref") with a Get function, and re-run the query.

Now that you have an idea of what your game documents look like, you can start creating REST calls, beginning with a POST.

Create a Serverless POST API Request

Your first API call is straightforward and shows off how Express combined with Cloud Functions allow you to serve all routes through one method.

Add this below the previous (and impeccable) API call:

api.get(['/api/v1', '/api/v1/'], (req, res) => {...})   api.post(['/api/v1/games', '/api/v1/games/'], (req, res) => {   let addGame = client.query(     q.Create(q.Collection('games'), {       data: {         title: req.body.title,         consoles: req.body.consoles,         metacritic_score: req.body.metacritic_score,         release_date: q.Date(req.body.release_date)       }     })   )   addGame     .then(response => {       res.status(200).send(`Saved! $ {response.ref}`)       return     })     .catch(reason => {       res.error(reason)     }) })

Please look past the lack of input sanitization for the sake of this example (all employees must sanitize inputs before leaving the work-room).

But as you can see, creating new documents in FaunaDB is easy-peasy.

The q object acts as a query builder interface that maps one-to-one with FQL functions (find the full list of FQL functions here).

You perform a Create, pass in your collection, and include data fields that come straight from the body of the request.

client.query returns a Promise, the success-state of which provides a reference to the newly-created document.

And to make sure it’s working, you return the reference to the caller. Let’s see it in action.

Test Firebase Functions Locally with Postman and cURL

Use Postman or cURL to make the following request against localhost:5000/api/v1/ to add Halo: Combat Evolved to your list of games (or whichever Halo is your favorite but absolutely not 4, 5, Reach, Wars, Wars 2, Spartan…)

$  curl http://localhost:5000/api/v1/games -X POST -H "Content-Type: application/json" -d '{"title":"Halo: Combat Evolved","consoles":["xbox","windows","os_x"],"metacritic_score":97,"release_date":"2001-11-15"}'

If everything went right, you should see a reference coming back with your request and a new document show up in your FaunaDB console.

Now that you have some data in your games collection, let’s learn how to retrieve it.

Retrieve FaunaDB Records Using a REST API Request

Earlier, I mentioned that every FaunaDB query requires an Index and that Fauna prevents you from doing inefficient queries. Since our next query will return games filtered by a game console, we can’t simply use a traditional `where` clause since that might be inefficient without an index. In Fauna, we first need to define an index that allows us to filter.

To filter, we need to specify which terms we want to filter on. And by terms, I mean the fields of document you expect to search on.

Navigate to Indexes in your FaunaDB Console and create a new one.

Name it games_by_console, set data.consoles as the only term since we will filter on the consoles. Then set data.title and ref as values. Values are indexed by range, but they are also just the values that will be returned by the query. Indexes are in that sense a bit like views, you can create an index that returns a different combination of fields and each index can have different security.

To minimize request overhead, we’ve limited the response data (e.g. values) to titles and the reference.

Your screen should resemble this one:

Under indexes, create a new index named games_by_console using the parameters above.

Click “Save” when you’re ready.

With your Index prepared, you can draft up your next API call.

I chose to represent consoles as a directory path where the console identifier is the sole parameter, e.g. /api/v1/console/playstation_3, not necessarily best practice, but not the worst either — come on now.

Add this API request to your index.js file:

api.post(['/api/v1/games', '/api/v1/games/'], (req, res) => {...})   api.get(['/api/v1/console/:name', '/api/v1/console/:name/'], (req, res) => {   let findGamesForConsole = client.query(     q.Map(       q.Paginate(q.Match(q.Index('games_by_console'), req.params.name.toLowerCase())),       q.Lambda(['title', 'ref'], q.Var('title'))     )   )   findGamesForConsole     .then(result => {       console.log(result)       res.status(200).send(result)       return     })     .catch(error => {       res.error(error)     }) })

This query looks similar to the one you used in your SHELL to retrieve all games, but with a slight modification.This query looks similar to the one you used in your SHELL to retrieve all games, but with a slight modification. Note how your Match function now has a second parameter (req.params.name.toLowerCase()) which is the console identifier that was passed in through the URL.

The Index you made a moment ago, games_by_console, had one Term in it (the consoles array), this corresponds to the parameter we have provided to the match parameter. Basically, the Match function searches for the string you pass as its second argument in the index. The next interesting bit is the Lambda function. Your first encounter with Lamba featured a single string as Lambda’s first argument, “ref.”

However, the games_by_console Index returns two fields per result, the two values you specified earlier when you created the Index (data.title and ref). So basically we receive a paginated set containing tuples of titles and references, but we only need titles. In case your set contains multiple values, the parameter of your lambda will be an array. The array parameter above (`[‘title’, ‘ref’]`) says that the first value is bound to the text variable title and the second is bound to the variable ref. text parameter. These variables can then be retrieved again further in the query by using Var(‘title’). In this case, both “title” and “ref,” were returned by the index and your Map with Lambda function maps over this list of results and simply returns only the list of titles for each game.

In fauna, the composition of queries happens before they are executed. When you write var q = q.Match(q.Index('games_by_console'))), the variable just contains a query but no query was executed yet. Only when you pass the query to client.query(q) to be executed, it will execute. You can even pass javascript variables in other Fauna FQL functions to start composing queries. his is a big benefit of querying in Fauna vs the chained asynchronous queries required of Firestore. If you ever have tried to generate very complex queries in SQL dynamically, then you will also appreciate the composition and less declarative nature of FQL.

Save index.js and test out your API with this:

$  curl http://localhost:5000/api/v1/xbox {"data":["Halo: Combat Evolved"]}

Neat, huh? But Match only returns documents whose fields are exact matches, which doesn’t help the user looking for a game whose title they can barely recall.

Although Fauna does not offer fuzzy searching via indexes (yet), we can provide similar functionality by making an index on all words in the string. Or if we want really flexible fuzzy searching we can use the filter syntax. Note that its is not necessarily a good idea from a performance or cost point of view… but hey, we’ll do it because we can and because it is a great example of how flexible FQL is!

Filtering FaunaDB Documents by Search String

The last API call we are going to construct will let users find titles by name. Head back into your FaunaDB Console, select INDEXES and click NEW INDEX. Name the new Index, games_by_title and leave the Terms empty, you won’t be needing them.

Rather than rely on Match to compare the title to the search string, you will iterate over every game in your collection to find titles that contain the search query.

Remember how we mentioned that indexes are a bit like views. In order to filter on title , we need to include `data.title` as a value returned by the Index. Since we are using Filter on the results of Match, we have to make sure that Match returns the title so we can work with it.

Add data.title and ref as Values, compare your screen to mine:

Create another index called games_by_title using the parameters above.

Click “Save” when you’re ready.

Back in index.js, add your fourth and final API call:

api.get(['/api/v1/console/:name', '/api/v1/console/:name/'], (req, res) => {...})   api.get(['/api/v1/games/', '/api/v1/games'], (req, res) => {   let findGamesByName = client.query(     q.Map(       q.Paginate(         q.Filter(           q.Match(q.Index('games_by_title')),           q.Lambda(             ['title', 'ref'],             q.GT(               q.FindStr(                 q.LowerCase(q.Var('title')),                 req.query.title.toLowerCase()               ),               -1             )           )         )       ),       q.Lambda(['title', 'ref'], q.Get(q.Var('ref')))     )   )   findGamesByName     .then(result => {       console.log(result)       res.status(200).send(result)       return     })     .catch(error => {       res.error(error)     }) })

Big breath because I know there are many brackets (Lisp programmers will love this) , but once you understand the components, the full query is quite easy to understand since it’s basically just like coding.

Beginning with the first new function you spot, Filter. Filter is again very similar to the filter you encounter in programming languages. It reduces an Array or Set to a subset based on the result of a Lambda function.

In this Filter, you exclude any game titles that do not contain the user’s search query.

You do that by comparing the result of FindStr (a string finding function similar to JavaScript’s indexOf) to -1, a non-negative value here means FindStr discovered the user’s query in a lowercase-version of the game’s title.

And the result of this Filter is passed to Map, where each document is retrieved and placed in the final result output.

Now you may have thought the obvious: performing a string comparison across four entries is cheap, 2 million…? Not so much.

This is an inefficient way to perform a text search, but it will get the job done for the purpose of this example. (Maybe we should have used ElasticSearch or Solr for this?) Well in that case, FaunaDB is quite perfect as central system to keep your data safe and feed this data into a search engine thanks to the temporal aspect which allows you to ask Fauna: “Hey, give me the last changes since timestamp X?”. So you could setup ElasticSearch next to it and use FaunaDB (soon they have push messages) to update it whenever there are changes. Whoever did this once knows how hard it is to keep such an external search up to date and correct, FaunaDB makes it quite easy.

Test the API by searching for “Halo”:

$  curl http://localhost:5000/api/v1/games?title=halo

Don’t You Dare Forget This One Firebase Optimization

A lot of Firebase Cloud Functions code snippets make one terribly wrong assumption: that each function invocation is independent of another.

In reality, Firebase Function instances can remain “hot” for a short period of time, prepared to execute subsequent requests.

This means you should lazy-load your variables and cache the results to help reduce computation time (and money!) during peak activity, here’s how:

let functions, admin, faunadb, q, client, express, cors, api   if (typeof api === 'undefined') { ... // dump the existing code here }   exports.api = functions.https.onRequest(api)

Deploy Your REST API with Firebase Functions

Finally, deploy both your functions and hosting configuration to Firebase by running firebase deploy from your shell.

Without a custom domain name, refer to your Firebase subdomain when making API requests, e.g. https://{project-name}.firebaseapp.com/api/v1/.

What Next?

FaunaDB has made me a conscientious developer.

When using other schemaless databases, I start off with great intentions by treating documents as if I instantiated them with a DDL (strict types, version numbers, the whole shebang).

While that keeps me organized for a short while, soon after standards fall in favor of speed and my documents splinter: leaving outdated formatting and zombie data behind.

By forcing me to think about how I query my data, which Indexes I need, and how to best manipulate that data before it returns to my server, I remain conscious of my documents.

To aid me in remaining forever organized, my catalog (in FaunaDB Console) of Indexes helps me keep track of everything my documents offer.

And by incorporating this wide range of arithmetic and linguistic functions right into the query language, FaunaDB encourages me to maximize efficiency and keep a close eye on my data-storage policies. Considering the affordable pricing model, I’d sooner run 10k+ data manipulations on FaunaDB’s servers than on a single Cloud Function.

For those reasons and more, I encourage you to take a peek at those functions and consider FaunaDB’s other powerful features.

The post Build a 100% Serverless REST API with Firebase Functions & FaunaDB appeared first on CSS-Tricks.

CSS-Tricks

, , , , , ,
[Top]