Building a Serverless Web App Using Firebase
In this article, we’ll see how Firebase allows us to focus on our core business logic and exposes simple to integrate APIs we can use to communicate with the backend.
In the simplest terms, Serverless frameworks and architectures enable you to build and run web applications without thinking about managing servers.
This frees up valuable time and resources that can be focused on working on the actual business logic of your application.
Firebase is a mobile and web development platform that helps you quickly develop high-quality apps and grow your business.
To demonstrate this, we’ll be building a conference application that provides authentication, allows users to upload images to the gallery, and post reviews and updates with live feeds.
You can check out the finished app here: confer-2020.web.app.
Setup
We’ll start by installing the firebase CLI with npm:
npm install -g firebase-tools
npm
is a node.js package manager that’s used to install Firebase CLI.
Once that is done, head over to the Firebase website to create a project that we’ll be referring to throughout the tutorial. A Google account (@gmail.com) is needed to be able to use Firebase. Once you’ve created a project, you would see a screen like so:
Authenticating Users
Our setup is ready so we can actually start writing some code. Before we get into the code, here’s a look at the folder structure.
For this application, React was used to build the frontend but the concepts can be applied in other JavaScript frameworks.
Let’s start with authentication which is a common need for applications that need to identify users and provide specialized services. On the Firebase console, add a new app:
It asks you a few questions about the app like the name, platform (web or mobile) and provides you with some information code to add to your app.
We’ll use this code in a moment so copy it and keep somewhere easily accessible. On the Firebase console, you need to enable the types of authentication you want to include in your app.
Under the authentication menu, switch to the sign-in method tab and enable the sign-in methods you need. For this app, we’ll be using Google and Email/password sign in.
That’s all that’s needed on Firebase. Let’s switch to our text editor to write some code.
In your terminal, switch to the project directory and run the following command
npm install --save firebase firebaseui
In the main entry point of the app, add the following:
// src/index.js
import firebase from 'firebase';
const firebaseConfig = {
apiKey: "INSERT_YOUR_DETAILS_HERE",
authDomain: "INSERT_YOUR_DETAILS_HERE.firebaseapp.com",
databaseURL: "https://INSERT_YOUR_DETAILS_HERE.firebaseio.com",
projectId: "INSERT_YOUR_DETAILS_HERE",
storageBucket: "INSERT_YOUR_DETAILS_HERE.appspot.com",
messagingSenderId: "INSERT_YOUR_DETAILS_HERE",
appId: "INSERT_YOUR_DETAILS_HERE",
measurementId: "INSERT_YOUR_DETAILS_HERE"
};
firebase.initializeApp(firebaseConfig);
Here we insert the Firebase output code from earlier into our app. This initializes firebase. To start Authenticating users, add the following code:
// src/views/LoginPage.js
import React from 'react';
import firebase from 'firebase';
import * as firebaseui from 'firebaseui';
export default class LoginPage extends React.Component {
constructor(props) {
super(props);
this.state = {
ui: new firebaseui.auth.AuthUI(firebase.auth()),
};
}
componentDidMount() {
this.state.ui.start('#firebaseui-auth-container', {
signInOptions: [
{
provider: firebase.auth.EmailAuthProvider.PROVIDER_ID,
requireDisplayName: true
},
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
],
signInSuccessUrl: '/'
})
}
componentWillUnmount() {
this.state.ui.delete();
}
render() {
return (
<div>
<section className="our-speaker-area section-padding-100-60">
<div className="container">
<div className="row">
<div className="col-md-12 mt-5 text-center">
<h2>Login to Confer</h2>
<p>Login to access more features like post feeds, upload images and more!</p>
<div id="firebaseui-auth-container"></div>
</div>
</div>
</div>
</section>
</div>
)
}
}
In the code snippet, there is some HTML markup which is quite straight forward. The thing to note there is <div id="firebaseui-auth-container"></div>
which would hold the UI for our sign-in providers. The sign-in flow is very common so the firebase team built a UI library that integrates nicely with firebase itself.
// src/views/LoginPage.js
constructor(props) {
super(props);
this.state = {
ui: new firebaseui.auth.AuthUI(firebase.auth()),
};
}
This code initializes firebaseui. Then we use it like so:
// src/views/LoginPage.js
componentDidMount() {
this.state.ui.start('#firebaseui-auth-container', {
signInOptions: [
{
provider: firebase.auth.EmailAuthProvider.PROVIDER_ID,
requireDisplayName: true
},
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
],
signInSuccessUrl: '/'
})
}
ui.start() takes two parameters: the DOM node to mount the generated UI and an object. The object specifies the sign-in options and where to redirect on a successful login. Notice we’ve specified Google and Email/password sign in here. The result of this code is:
This is what a user would see on the demo site. Once any button is clicked, it takes the user through the entire authentication flow and signs them up.
Hosting your web app
Firebase provides a simple and intuitive way to host your web app. In your terminal, run the following commands:
firebase login
firebase init
The login command authenticates you so you can interact with the Firebase platform directly from the terminal. The init command should be run inside your project directory. You should also select the project created earlier on the firebase console.
The firebase documentation provides a detailed explanation of hosting your web app. Once done, we deploy the app to the firebase.
firebase deploy
Creating an Image Gallery
For the demo site, we need a gallery section where attendees and conference organizers can upload photos from the conference. Firebase includes cloud storage where media assets can easily be stored. Let’s walk through how that is done.
On Firebase console, switch to storage.
Select “Get Started” and follow the steps. The final step should look like this:
Firebase storage has rules which determine who can access content in the storage. By default, this rule states that all authenticated users can read and write. For this demo app, this rule is fine.
Back in the text editor, we’ll create a component for the gallery.
// src/views/GalleryPage.js
import firebase from 'firebase';
We start by importing Firebase.
// src/views/GalleryPage.js
handleUpload(event) {
const currentTime = Date.now();
const storageRef = firebase.storage().ref();
const uploadImageRef = storageRef.child(`gallery/image-${currentTime}`);
const uploadTask = uploadImageRef.put(file);
uploadTask.on('state_changed', (snapshot) => {
let progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log('Upload is ' + progress + '% done');
});
}
We create a reference to firebase storage. Then specify the path we want to write to (gallery/image-path). We create a task which fires a state_changed event as the image is being uploaded. We can use the values to update a progress bar or any form of loading indicator in our app.
This function uploads an image to firebase storage. To read from it, we can write another function:
// src/views/GalleryPage.js
async fetchImage() {
const storage = firebase.storage();
const storageRef = storage.ref();
const images = await storageRef.child('gallery').list({ maxResults: 100 });
images.items.forEach(async (item) => {
const imagePath = storageRef.child(item.location.path);
const imgURL = await imagePath.getDownloadURL();
this.updateImages(imgURL);
});
}
Again, we create a reference to firebase storage. Then read a maximum of 100 images at a time. This provides a simple way to do something like pagination or infinite scrolling with firebase. This returns an array we can loop through to get the download URL of each image. The URLs are stored in a local state variable.
// src/views/GalleryPage.js
updateImages(url) {
this.setState({
images: [...this.state.images, { url }],
});
}
We can loop through this and create the DOM.
We want only authenticated users to be able to upload images so we can check if a user is authenticated like so:
// src/views/GalleryPage.js
firebase.auth().onAuthStateChanged(currentUser => {
if (currentUser) {
// show upload controls
}
});
This can be extended to add functionality like delete, update or even edit uploaded images but that’s beyond the scope of this article.
Going Serverless with Cloud Functions
The last thing we’ll add to this demo app is live feeds. A way for users to drop reviews and thoughts about what’s happening throughout and beyond the conference. We can interact directly with the Firebase database but instead, we’ll write a cloud function that would site in between our app and the database. To get started, cloud functions fire up your terminal and run firebase init
in your project directory.
Select Functions: Configure and deploy Cloud Functions and press “Enter.” Accept to install packages. Once it’s done, you would see new functions directed created in your project. Open up index.js in that folder so we’ll write our first cloud function.
// functions/index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.updateFeeds = functions.https.onCall((data, context) => {
const text = data.text;
const timestamp = data.timestamp;
const uid = context.auth.uid || '';
const name = context.auth.token.name || null;
return admin.database().ref('/feeds').push({
text,
timestamp,
author: { uid, name },
}).then(() => {
return { message: 'Feeds updated' }
});
});
We import firebase-admin and firebase-functions which are both required. Then we initialize firebase-admin.
If you are familiar with node/express functions, this would look familiar. Let’s break down the function.
We create a function updateFeeds. firebase.functions. onCall() because we’ll be calling it from our app. onCall() receives two parameters: data, which would be entered by the user and a context, which determines contains information about the current user such as their user ID and name.
To deploy the cloud function to firebase, run this in your terminal:
firebase deploy --only functions
We’re using the firebase realtime database which is a NoSQL-like database. So the database is structured as key-value pairs.
We push the data passed and context to /feeds path. The push() generates a unique key under /feeds and adds the object to it.
src/views/FeedsPage.js
componentDidMount() {
firebase.database().ref('/feeds').orderByChild('timestamp').on('value', (snapshot) => {
const tempFeed = [];
snapshot.forEach(childSnapshot => {
const obj = childSnapshot.val();
tempFeed.push(obj);
});
this.setState({
feeds: tempFeed,
});
})
}
In our app, we read all feeds from the database and populate the app. on('value', (snapshot) => {})
creates a connection to the server so when I new value is added to the database, the code block is automatically run again.
To write to the database, add the following code:
// src/components/FeedsForm.js
handleSubmit(e) {
const updateFeeds = firebase.functions().httpsCallable('updateFeeds');
updateFeeds({
text: this.state.text,
timestamp: Date.now(),
})
.then(result => console.log(result))
.catch(error => console.log(error));
}
We created a callable function in our cloud functions so we can call it by name from our app. Then we can simply pass the data from our app to the function. Firebase realtime database also has rules which determine who can access what. Here is what our database looks like:
This allows anyone that visits to be able to read but only authenticated users are allowed to write to the database.
Conclusion
These are just a few things that are possible using Firebase. We can extend the cloud function to filter out curse words in uploaded texts using machine learning algorithms or write new cloud functions to send out push notifications to those subscribed to notifications and much more. If you have any questions, I’ll be in the comments section below.