Developing a Serverless Web Application Completely on Google Cloud (2 of 2)

Brian Jung
Google Cloud - Community
11 min readSep 16, 2022

Building a serverless web application with Firebase and NodeJS without downloading anything!

Hi! Welcome back. We will now explore how we can use Cloud Storage, Firebase, App Engine and Cloud Functions on Google Cloud!

Database and Storage

Uploading to Firestore in Native Mode and Cloud Storage

Your application is not complete with just authentication. Let’s make the application more interactive with content and more features. We will create a post page where users can post a picture with captions. To store user information and keep them in a structured manner, we will be using Cloud Firestore, a serverless document database that effortlessly scales to meet any demand with no maintenance. For storing static files like images, we will be using Cloud Storage.

To begin, let’s go back to the firebase-config.js. Here we want to add getFirestore and getStorage from firebase firestore and storage libraries respectively. These will allow us to access these Cloud Services from your application. Then you have to wrap your initialized firebase application, to get storage and db that points to your Cloud Firestore and Storage.

//firebase-config.js
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
import { getAuth, GoogleAuthProvider } from 'firebase/auth';
import { getFirestore } from "firebase/firestore";
import { getStorage } from "firebase/storage";

// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "[YOUR API]",
authDomain: "[YOUR AUTH DOMAIN]",
projectId: "[YOUR PROJECT ID]",
storageBucket: "[YOUR STORAGE BUCKET]",
messagingSenderId: "[YOUR MESSAGING SENDER ID]",
appId: "[YOUR APP ID]"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
// Adding Storage and Database
export const storage = getStorage(app);
export const db = getFirestore(app);
// Adding Authentication
export const auth = getAuth(app);
export const provider = new GoogleAuthProvider();

Next, let’s create a collection in Cloud Firestore. This can be done on either Firebase Cloud Firestore page or Google Cloud console Firestore page. Click on Start Collection and name the collection posts and leave everything else empty. This is where we will store information about user’s posts.

Now we have to add a functionality to upload new post information through the application. First, import db, storage, and auth from the firebase-config file. Import addDoc and collection from firestore. Import ref, uploadBytesResumable, getDownloadURL from storage.

We will create states to store different field information (title, file, postInfo, storagesrc, likes and mostrecentliker). Also go ahead and create a state called percent for storing upload percent information. Finally, create required helper functions and add to the application. The code should look something like this.

// PostPg.js
import React, { useState } from "react";
import { addDoc, collection } from 'firebase/firestore'
import { ref, uploadBytesResumable, getDownloadURL } from "firebase/storage";
import { db, auth, storage } from '../firebase-config'
import { useNavigate } from "react-router-dom";

function PostPg({authState}) {
//Using Navigate to navigate away from this page to Home once post is uploaded
let navigate = useNavigate();
//Creating States for all the fields
const [title, setTitle] = useState("");
const [file, setFile] = useState("");
const [postInfo, setPostInfo] = useState("");
const [storagesrc, setStorageSrc] = useState("");
const likes = 0;
const mostrecentliker = "";
const [percent, setPercent] = useState(0); //For storage upload %

//Reference the collection "posts" you just created in the console
const postsCollectionRef = collection(db, "posts")

//Create a function to add a Document to the posts collection
const postToCloud = async () => {
await addDoc(postsCollectionRef, {
title,
postInfo,
author: {name: auth.currentUser.displayName, id: auth.currentUser.uid},
storagesrc,
likes,
mostrecentliker
});
navigate("/");
};

//Create a helper function to target file
function getFile(event) {
setFile(event.target.files[0]);
}

//Create a function to add a static image to Cloud Storage
function uploadToCloud() {
const storageRef = ref(storage,`/postImg/${file.name}`) //This reference to where files will save
const uploadTask = uploadBytesResumable(storageRef, file);
uploadTask.on(
"state_changed",
(snapshot) => {
const curPercent = Math.round((snapshot.bytesTransferred/snapshot.totalBytes)*100);
setPercent(curPercent); //This is what shows the percent uploaded
},
(err) => console.log(err),
() => {
getDownloadURL(uploadTask.snapshot.ref).then((url) => {
setStorageSrc(url) //This sets the storagesrc as the url to this picture!
});
}
);
}

return (
<div className="PostPg">
<div className="FieldInput">
<label>Title</label>
<input
placeholder="Title..."
onChange={(event) => {
setTitle(event.target.value);
}}
/>
</div>
<div className="FieldInput">
<label>Info:</label>
<textarea
placeholder="Info..."
onChange={(event) => {
setPostInfo(event.target.value);
}}
/>
</div>
<input type="file" onChange={getFile} accept="" />
<button onClick={uploadToCloud}>Upload Picture</button>
<div> Upload Progress: {percent}% </div>
<button onClick={postToCloud}> Submit Post </button>
</div>
);
}
export default PostPg;

You can play around with what you have so far by doing npm start and going to the development build. Take your time to read through the code to understand what each function is doing. Also go check our Firestore page on Google Cloud console or Cloud Firestore page on Firebase console. You should notice every post is creating a new document in your posts collection. You will also notice that you have a new postImg folder in your Cloud Storage, this is where all the pictures are saved.

Getting Information from Firestore in Native Mode and Cloud Storage

Just sending information and images to the cloud is not enough to create an interactive web application, we also need to be able to access this information. Let’s create a home page where users can see all the posts everyone posted. First create a state for the list of posts, that will be refreshed by using useEffect from React. Within the useEffect, we will be using the collection posts and store all the data into a list called posts. Then we can display data using the information for each specific post, in the list posts. It will look something like this.

//HomePg.js
import React from "react";
import {getDocs, collection} from 'firebase/firestore';
import { db } from '../firebase-config';

function HomePg({authState}) {
const [posts, setPosts] = useState([]);

// This function runs when the page is loaded to get the data. It stores the data in to the posts state (type list).
useEffect(()=> {
const getData = async () => {
const data = await getDocs(collection(db, "posts"));
setPosts(data.docs.map((doc) => ({...doc.data(), id: doc.id })));
};
getData();
});

return (
<div className="HomePg">
{posts.map((post)=> {
return (
<div className="post" key={post.id}>
<h3>{post.author.name} writes about {post.title}</h3>
<div className="textContainer">
{post.postInfo}
</div>
{(post.storagesrc !== null && post.storagesrc !== undefined && post.storagesrc !== "") &&
<div className="box">
<img src={post.storagesrc} alt="" ></img>
</div>
}
</div>
)
})}
</div>
);
}
export default HomePg;

You can see how you can access information for each post. You can also see an example of how to add logic so that the image is only shown if there is a proper value in the storagesrc field.

You have the backend logic that shows all the posts now, but let’s add some finishing touches. Add a like button that only shows up if you’re logged in and a delete button for your own posts. Adding these final features will make your code look like this.

//HomePg.js
import
React, { useEffect, useState } from "react";
import { getDocs, collection, deleteDoc, doc, updateDoc, increment } from 'firebase/firestore';
import { db, auth } from '../firebase-config';

function HomePg({authState}) {
const [posts, setPosts] = useState([]);

// This function runs when the page is loaded to fetch the data.
// It stores the data into the posts state (type list).
useEffect(()=> {
const getData = async () => {
const data = await getDocs(collection(db, "posts"));
setPosts(data.docs.map((doc) => ({...doc.data(), id: doc.id })));
};
getData();
});

//Delete your own posts function
const deletePost = async (id) => {
const thisSpecificDoc = doc(db, "posts", id);
await deleteDoc(thisSpecificDoc);
}

//Like your friends post = It adds one like and also takes note of your email
const likePost = async (id, emails) => {
const thisSpecificDoc = doc(db, "posts", id);
await updateDoc(thisSpecificDoc,{likes: increment(1), mostrecentliker:emails});
}

return (
<div className="HomePg">
{posts.map((post)=> {
return (
<div className="post" key={post.id}>
<div className="like">
{auth.currentUser != null &&
<div> {post.author.id !== auth.currentUser.uid &&
<button onClick={() => {likePost(post.id, auth.currentUser.email)}}> &#10084; </button> }
<div> {post.likes} </div>
</div>
}
</div>
<div className="delete">
{auth.currentUser != null &&
<div> {post.author.id === auth.currentUser.uid &&
<button onClick={() => {deletePost(post.id)}}> Delete </button>}
</div>
}
</div>
<h3>{post.author.name} writes about {post.title}</h3>
<div className="textContainer">
{post.postInfo}
</div>
{(post.storagesrc !== null && post.storagesrc !== undefined && post.storagesrc !== "") &&
<div className="box">
<img src={post.storagesrc} alt="" ></img>
</div>
}
</div>
)
})}
</div>
);
}
export default HomePg;

Feel free to add CSS to App.css to make the application look better, I’m not a big fan of working with CSS so I will not be providing any code for CSS.

Congratulations! You have successfully created a photo/post sharing application. Please be wary of posting too many posts and uploading too many photos can incur charges to your account. Push all your recent changes to have it saved in your Cloud Repository.

Deploying Project on App Engine

Building and deploying the project

You feel like the application you just created is good enough to be published to the public, but what is an easy way to deploy? Using App Engine, users can deploy NodeJS applications with zero server management and zero configuration deployments. Let’s prepare to deploy by enabling the Cloud Build API, and initializing your App Engine app with your project. Note that you can only have one App Engine application running per Project, and App Engine apps are regional. If latency matters for your application, choose the region closest to your users as the region cannot be changed in the future.

gcloud app create --region=us-central

Next let’s build your React application. You can easily do this by running the following.

npm run build

This command keeps your app the same but creates a compacted small package called build. Now you can try to deploy this production build of your app. Before trying to deploy on App Engine, let’s specify how URL paths correspond to request handlers and static files and runtime for our application. Create an app.yaml file inside the my-app directory. It should look like this.

env: standard
runtime: nodejs10
service: react-prod
handlers:
- url: /static
static_dir: build/static
- url: /(.*\.(json|ico|js))$
static_files: build/\1
upload: build/.*\.(json|ico|js)$
- url: .*
static_files: build/index.html
upload: build/index.html

Once you save the app.yaml file, go back to Cloud Shell and run deploy in the my-app directory.

gcloud app deploy

This should give you the details of the services you are deploying, it also gives you the target url where your application will be deployed. Remember to add this link to the Authorized Domains list to ensure that the authentication part works properly. You can run app browse to get this link again.

gcloud app browse -s react-prod

If you go to the App Engine page on your Google Cloud console, you should see your application running with Status serving, and traffic allocation of 100%.

Version control for your application

App Engine allows you to control the versions of your application and traffic as well. Go ahead and run the deploy command again.

gcloud app deploy

The App Engine should show that your react-prod has two different versions. Press on the number 2 under version (or go to the Version page from the left navigation bar). This should show that the newest version has 100% of traffic allocation. You can go back to the first version by pressing on its checkbox and clicking on Migrate Traffic. This will send all traffic to the first version of your application. If you want to do a slow roll out of your newest version, you will select both versions and click split traffic and have the old version get 90% and the new version 10% of the traffic

Using Cloud Functions

Creating stand alone functions that responds to events

Google Cloud Functions is a serverless execution environment for building and connecting cloud services. You can use it to create functionalities that will execute without the need of any infrastructure management. Let’s create a function that sends an email to the 25th person to like a specific post. Go to the Google Cloud console and go to the Cloud Functions page.

Now click on Create Function and pick 1st gen environment, and name the function email-sender-function. Next select Cloud Firestore as trigger type, and event type update. The Document path will be posts/{docid}. We are using wildcards here because we want to be able to look for any updates of all the posts. Leave the retry on failure unchecked.

When you click next, you will be asked to create the code. Select Node.js 16 for the runtime and helloFirestore as Entry point. Then create the following two files. You will notice that you don’t have a sgMail API key (sendgrid), we will come back and edit this later.

package.json

{
"name": "Sample Example",
"version": "1.0.0",
"engines": {
"node": ">8.0.0"
},
"description": "Cloud Function that sends email with logic",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Brian Jung",
"dependencies": {
"@google-cloud/firestore": "^0.19.0",
"@sendgrid/mail": "^6.3.1"
}
}

index.js

const sgMail = require('@sendgrid/mail');
sgMail.setApiKey('[YOUR SGMAIL API KEY]');

exports.helloFirestore = (event, context) => {
const eventId = event.context.eventId;
const emailRef = db.collection('sentEmails').doc(eventId);

return shouldSendWithLease(emailRef).then(send => {
if (send) {
//Send email if the like field returns 25
if (event.value.fields.likes.integerValue == 25) {
const msg = {
to: event.value.fields.mostrecentliker.stringValue, // This is email of most recent liker
from: '[YOUR EMAIL HERE]', // Use the email address or domain you verified above
subject: 'You Liked a Post',
text: 'You were the 25th person to like a specific post!',
html: '<strong>You were the 25th person to like a specific post!</strong>',
};
sgMail.send(msg)
return markSent(emailRef)
};
}
});

};

const leaseTime = 60 * 1000; // 60s

function shouldSendWithLease(emailRef) {
return db.runTransaction(transaction => {
return transaction.get(emailRef).then(emailDoc => {
if (emailDoc.exists && emailDoc.data().sent) {
return false;
}
if (emailDoc.exists && admin.firestore.Timestamp.now() < emailDoc.data().lease) {
return Promise.reject('Lease already taken, try later.');
}
transaction.set(
emailRef, {lease: new Date(new Date().getTime() + leaseTime)});
return true;
});
});
}

function markSent(emailRef) {
return emailRef.set({sent: true});
}

The package.json file is telling the NodeJS runtime that you need the firestore and sendgrid dependencies. The index.js is your main file, and it is looking at the events that trigger the function. It looks specifically at the value of the field ‘likes’ and compares it with the number 25. If they are equal, the function will use sendgrid to send an email to the 25th person to like the post or in our case the stringValue of the mostrecentliker (we stored the emails of the most recent user to like the post).

The messaging system between Firestore and Cloud Functions trigger is a Google Cloud Service called Pub/Sub. Pub/Sub provides at-least-once delivery but might notify your Function more than once. This code is idempotent to ensure that the 25th user only receives one email. Read more about idempotency here.

Now we have almost everything set up! In Google Cloud console go to Marketplace and look for SendGrid Email API. Google Cloud Marketplace is where you can connect third party tools to work with your cloud solution. We will be using SendGrid, an email delivering service. In the SendGrid Email API page, purchase the Free option. You will have to create an account with SendGrid and get the SendGrid API key, save this to a secure location you have access to. Head back to Cloud Function and edit your email-sender-function. Go to code and change [YOUR SGMAIL API KEY] with the API key you just created.

Congratulations! If you go to your App Engine deployment website and like a post until it reaches 25 likes, you will get an email from Cloud Function titled “You Liked a Post”.

Cleaning Up

Feel free to continue playing around with Google Cloud. Thank you for going through the guide, I hope you learned something new about application development.

Disable your application

First we will disable the SendGird Email API. Go back to Cloud Marketplace and go to the SendGrid Email API page. Scroll down to Pricing, and you will see a manage order button. Press on the button and you will be redirected to your orders for the specific product. Press the “more” button at the right end of your order (Hint: it looks like three dots) and press on Disable Auto Renewal, and Cancel Order.

Next, head to the App Engine page. Go to the setting and click Disable application. In the App ID field, enter the ID of the app you want to disable, and then click Disable. This should disable your app engine services and also stop your firestore.

Delete your project

To release all the Google Cloud resources in your Cloud project, delete your project. Go to IAM -> Manage resources page. Highlight your project and click Delete. In the Project ID field, enter the ID of the project you want to delete, and then click Delete.

--

--