Deploy Node.js Apps on Google App Engine

Introduction

TL;DR; In this article we are going to deploy a Node.js app on Google App Engine and in the process see how it is done.

This is going to be a step-by-step demonstration starting from setting up our Google App Engine environment to deployment.

NB: This tutorial requires a basic understanding of JavaScript, Node.js, MongoDB, NPM, and Express.js.

You can get the source code of the finished app here.

What is Google App Engine?

Google App Engine, a (PaaS) cloud-computing platform released by Google on 7th April 2008 is written in C++, PHP, Node.js, and Python.

Google App Engine provides an ideal environment for developers and organizations to host their applications without having to think about the infrastructure, downtime, or scaling to a billion users. Google App Engine provides all of this, and in truth, you need not worry about the server, just deploy and Google App Engine will handle most everything else. Google App Engine will automatically scale and allocate more resources to your app when requests and demand are huge.

Google App Engine is a cloud runtime environment that allows you to easily deploy and run standard web apps. It provides the tools for monitoring, scaling, and load balancing infrastructure, so you can focus on building your web apps instead of the servers that run them.

Create a Google App Engine Instance (2nd Gen. Instance)

To start using Google App Engine, we are going to setup a Google Cloud Platform project:

  1. Sign in to your Google account. If you don't already have one, you should sign up.
  2. Go to the App Engine website
  3. A dialogue may appear demanding to use the App version of Google Cloud Console: "Use app" or "Not now". It's up to you to make your choice, but preferably, click on "Not now" to continue.
  4. On the screen that shows up, it will present two options: "Create" or "Select". For this tutorial, we are creating a new project, click on the "Create" button. If you have exceeded the maximum number of your GCP projects quota, you should "Select" a project.
  5. Type your project name on the "Project name" text field, below the text field will be your project ID generated by GCP based on your Project name. click on "Create" button when done.
  6. After few seconds, a screen shows up to "Select a location". On the "Select a region" dropdown widget, click on it to select your preferred region, then click on "Next".
  7. Next screen shows up to "Enable billing". Click on "Set up billing".
  8. A modal dialogue shows up, click on "Create billing account".
  9. Type in your preferred billing account name on the next window or you can go with the default name.
  10. Select your country, USD has been selected as default as currency, click on the "Confirm" button.
  11. On the next window, fill in your details, both personal and bank account details
  12. Click on "Submit and enable billing" button. Now, we have created a Google Cloud project with billing enabled.

Now, we are done!

Installing the Google Cloud Tools (Cloud SDK)

Google Cloud tools is a bag full of utilities that are all very useful in setting up and accessing Google Cloud products: Google Kubernetes, Google App Engine, Google Big Query from your terminal. To begin installing the Cloud SDK, go to Google Cloud SDK, and download the SDK installer for your OS.

Google Cloud SDK contains tools like gcloud, and gsutil, but we will be using the gcloud tool to initialize and deploy our app.

The gcloud tool contain various commands to enable users to perform different actions on a Google Cloud Project:

  • gcloud info: Displays information about your Cloud SDK, your system, the logged in user and the currently active project.
  • gcloud auth list: Displays list of Google accounts active in the Cloud SDK.
  • gcloud init: initializes a Google cloud Project.
  • gcloud help: Displays commands available in gcloud and their usage.
  • gcloud config list Displays the list of the gcloud configurations.

OK, we have digressed a little bit, lets come back to what we have in hand, after downloading the Cloud SDK installer, launch the installer and follow the prompts, make sure you check relevant options presented. After the installation has completed the installer will launch the command gcloud init in a terminal window.

This command will take you through series of configuration. It will present you an option to log in:

You must log in to continue. Would you like to log in (Y/n)?  

Type "Y" and hit the Enter key. It will launch your default web browser, where you will select your preferred Google account. After that, it will display on the terminal list of your Google projects:

You are logged in as [YOUR_GOOGLE_ACCOUNT_EMAIL]:

pick cloud project to use:  
 [1] [YOUR_PROJECT_NAME]
 [2] Create a new project
Please enter numeric choice or text value (must exactly match list item):  

NB: gcloud will automatically select, if you have only one project.

Next, you are prompted to choose a default Compute Engine zone:

Which Google Compute Engine zone would you like to use project default:  
 [1] asia-east1-a
 ...
 [16] us-east1-b
 ...
 [25] Do not select default zone
Please enter numeric choice or text value (must exactly match list item):  

After selecting your default zone, gcloud does a series of checks and prints out:

Your project default Compute Engine zone has been set to [YOUR_CHOICE_HERE]  
You can change it by running [gcloud config set compute/zone NAME]

Your project default Compute Engine region has been set to [YOUR_CHOICE_HERE]  
You can change it by running [gcloud config set compute/region NAME]  

Your Google Cloud SDK is configured and ready to use!

Setup our Node.js app

Now, our Google Cloud project has been configured. Let's setup our Node.js app. We are going to create a RESTful API for the movie Black Panther. Wooh!!! This will be great. On Feb 16, 2018 the first Marvel black superhero movie was premiered in cinemas around the world, garnering a massive $903 million in box office, as of the time of this writing, making it the 45th highest grossing movie of all time and highest grossing movie in 2018.

Let's build an API that will return the characters of the Black Panther.

API endpoint

  1. Character - This resource is about the Black Panther characters.

    • POST - /blackpanther/ Creates a new Black Panther instance.
    • GET - /blackpanthers/ Returns all Black Panther characters.
    • GET - /blackpanther/<id> Returns the specified Black Panther character id.
    • PUT - /blackpanther/<id> Update a Black Panther character attributes.
    • DELETE - /blackpanther/<id> Delete a Black Panther character.

Black Panther Character Model Structure

{
    "alias": String,
    "occupation": String,
    "gender": String,
    "place_of_birth": String,
    "abilities": String,
    "played_by": String,
    "image_path": String
}

Create API Endpoints for the Black Panther API

To start off, let's start by creating our project folder, open your terminal and run the following command:

$ mkdir _nodejs_gae

Next, move into the folder:

$ cd _nodejs_gae

Node.js app is initialized using the npm init command. Now, we are inside our project folder, run the following command to instantiate a Node.js app:

$ npm init -y

This commands creates a Node.js app using your pre-configured credentials. By now your folder will be looking like this:

|- _nodejs_gae
    |- package.json

To follow best practices, we are going to divide our app into Controllers, Models, and Routes. Yeah, I know it's overkill for this demo app but it is always good to do it right.

Let's create our index.js file (our server entry-point) - touch index.js

Create the following folders:

  • mkdir routes
  • mkdir ctrls
  • mkdir models

We now have routes, ctrls, and models folders.

  • routes: Will hold all the routes defined in our API and it will call the controller function assigned to the matching HTTP request.
  • ctrls: Will hold the action to get the requested data from the models.
  • models: Will hold the Database Model of our API.

We are going to have one route, one model and, one controller associated with our API. Run the following commands to create the files:

  • touch routes/route.js
  • touch ctrls/ctrl.js
  • touch models/Character.js

Our folder structure should look like this now:

|- _nodejs_gae
    |- routes/
        |- route.js
    |- ctrls/
        |- ctrl.js
    |- models/
        |- Character.js
    |- index.js
    |- package.json

OK, let's install our dependencies:

  • npm i express -S
  • npm i mongoose -S
  • npm i body-parser -S

We now, open up our Character.js and paste the following code into it:

const mongoose = require('mongoose')

let Character = new mongoose.Schema({  
    alias: String,
    occupation: String,
    gender: String,
    place_of_birth: String,
    abilities: String,
    played_by: String,
    image_path: String
})
module.exports = mongoose.model('Character', Character)  

Here, we declared our model schema Character using mongoose Schema class. Our model was the exported so that we can import and use the Schema anywhere in our app.

Ok, let's add code to our ctrl.js file:

const Character = require('./../models/Character')

module.exports = {  
    getCharacter: (req, res, next) => {
        Character.findById(req.params.id, (err, Character) => {
            if (err)
                res.send(err)
            else if (!Character)
                res.send(404)
            else
                res.send(Character)
            next()
        })
    },
    getAllCharacters: (req, res, next) => {
        Character.find((err, data) => {
            if (err) {
                res.send(err)
            } else {
                res.send(data)
            }
            next()
        })
    },
    deleteCharacter: (req, res, next) => {
        Character.findByIdAndRemove(req.params.id, (err) => {
            if (err)
                res.send(err)
            else
                res.sendStatus(204)
            next()
        })
    },
    addCharacter: (req, res, next) => {
        (new Character(req.body)).save((err, newCharacter) => {
            if (err)
                res.send(err)
            else if (!newCharacter)
                res.send(400)
            else
                res.send(newCharacter)
            next()
        })
    },
    updateCharacter: (req, res, next) => {
        Character.findByIdAndUpdate(req.params.id, req.body, (err, updatedCharacter) => {
            if (err)
                res.send(err)
            else if (!updatedCharacter)
                res.send(400)
            else
                res.send(req.body)
            next()
        })
    }
}

Here, declared our 4 CRUDy functions: getCharacter, deleteCharacter, getAllCharaccters, and updateCharacter. Like their names implies they perform CREATE, READ, UPDATE and DELETE actions on our Black Panther API.

OK, let's open up the route.js file and paste the following code in it:

const ctrl = require('./../ctrls/ctrl')

module.exports = (router) => {

    /** get all Black Panther characters */
    router
        .route('/blackpanthers')
        .get(ctrl.getAllCharacters)

    /** save a Black Panther character */
    router
        .route('/blackpanther')
        .post(ctrl.addCharacter)

    /** get a Black Panther character */
    router
        .route('/blackpanther/:id')
        .get(ctrl.getCharacter)

    /** delete a Black Panther character */
    router
        .route('/blackpanther/:id')
        .delete(ctrl.deleteCharacter)

    /** update a Black Panther character */
    router
        .route('/blackpanther/:id')
        .put(ctrl.updateCharacter)
}

Above we have defined two basic routes(/blackpanther, and /blackpanther/:id) with different methods.

As we can see, we required the controller so each of the routes methods can call its respective handler function.

Finally, we open up our index.js file. Here, we bind the components into one. We import the routes function exposed to us in routes/route.js, and we pass express.Router() as an argument to our routes function. Next, we connect to a MongoDB instance and, then call the app.listen() method to start the server.

const express = require('express')  
const mongoose = require('mongoose')  
const bodyParser = require('body-parser')

const app = express()  
const router = express.Router()  
const routes = require('./routes/route')

const url = process.env.MONGODB_URI || "mongodb://localhost:27017/blackpanther"

mongoose.connect(url, {  
    //useMongoClient: true
})

routes(router)  
app.use(bodyParser.json())  
app.use('/api/v1', router)

const port = process.env.PORT || 1000

app.listen(port, () => {  
    console.log(`Black Panther API v1: ${port}`)
})

Add mLab Datastore to our API Endpoints

All this time we have been using a local instance of MongoDB datastore. We will be deploying and accessing our app on a cloud-computing infrastructure, so there will be no local datastore present. In order to persist our data we are going to choose a Data as a service (DaaS) platform, mLab.

  • Go to mLab
  • Create an account, if you don't already have one
  • Go to your dashboard, create a new database
  • Copy the database connection URL

Now that we have our mLab connection URL string, we will now modify index.js file:

...
const url = process.env.MONGODB_URI || "mongodb://<DB_USER>:<DB_PASSWORD>@<MLAB_URL>.mlab.com:<MLAB_PORT>/<DB_NAME>"  
...

Test our App Locally via cURL

To test our app on our local machine. Run the following command to start the server:

$ node .

It will display something like this on your terminal:

$ node .
Black Panther API v1: 1000  

OK, now our Black Panther API is up and running, we can use cURL to test the APIs. Here we'll POST to the API to create a new Black Panther character:

curl --request POST \  
  --url http://localhost:1000/api/v1/blackpanther \
  --header 'content-type: application/json' \
  --data '{"alias":"tchalla","occupation":"King of Wakanda","gender":"male","place_of_birth":"Wakanda","abilities":"enhanced strength","played_by":"Chadwick Boseman"}'

As a task for the reader, you should go on and write cURL commands for other API endpoints as well.

Deploy our app

Now, our nodejs app is ready for deployment, but before we do that, there are configurations that we have to tweak and add. First, we are going to create an app.yaml file to our project.

The app.yaml file is a runtime configuration for App Engine environment. app.yaml enables us to configure our App Engine environment (either Node.js, GO, PHP, Ruby, Python, .NET, or Java Runtime) prior to deployment.

With app.yaml file, we can do the following:

  • Allocate network and disk resources
  • Select the flexible environment
  • Select the number of CPU cores to be allocated
  • Specify memory_gb (RAM) size

The list is long, you can go to the resource Configuring your app with app.yaml to see the full configuration settings curated by Google.

OK, let's create app.yaml file in our project:

touch app.yaml  

Open the app.yaml file, and add the following contents:

runtime: nodejs  
env: flex

manual_scaling:  
  instances: 1
resources:  
  cpu: 1
  memory_gb: 0.5
  disk_size_gb: 10

Looking at the above configuration, we are telling the App Engine that our app will run on the Node.js runtime environment, also the environment should be set to flexible.

Running on flexible environment incurs costs, so we scaled down to reduce costs by adding:

...
manual_scaling:  
  instances: 1
resources:  
  cpu: 1
  memory_gb: 0.5
  disk_size_gb: 10

Here, we are specifying only one instance, one CPU core, 0.5G RAM, and 10G disk size.

This is ideal for testing purposes and is not for production use.

Next, we have to add a start in the scripts section of our package.json, this is used by the Node.js runtime to start our application when deployed.

Without the start property, the Node.js runtime checker will throw "Application Detection failed: Error: nodejs checker: Neither start in the scripts section in the package.json nor server.js were found" error.

Let's open up the package.json and add start in the scripts key:

...
    "scripts": {
        "start": "node .",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
...

After this, we are now all set to deploy. To deploy our app, run this command:

$ gcloud app deploy

Testing our Deployed App with cURL

To test our deployed Node.js app API, we will need to use the target url Google App Engine gave us.

curl --request POST \  
  --url http://YOUR_TARGET_URL.appspot.com/api/v1/blackpanther \
  --header 'content-type: application/json' \
  --data '{"alias":"tchalla","occupation":"King of Wakanda","gender":"male","place_of_birth":"Wakanda","abilities":"enhanced strength","played_by":"Chadwick Boseman"}'

With cURL we sent a POST request and a Black Panther character payload to our deployed Node.js app, using the target url as our url parameter.

Our API endpoint executes the POST function, saves the payload to our mLab database and sends the result back to us:

{
    "alias":"tchalla",
    "occupation":"King of Wakanda",
    "gender":"male",
    "place_of_birth":"Wakanda",
    "abilities":"enhanced strength",
    "played_by":"Chadwick Boseman","_id":"5aa3a3905cd0a90010c3e1d9",
    "__v":0
}

Congratulations! We have successfully deployed our first Node.js app to Google App Engine.

Conclusion

We have seen in this article, how easy and stress-free Google App Engine make our lives. Also, how with only a few commands setups a powerful runtime engine and deploy your app on it. No need to think about scaling, resources, bandwidth and the rest.

App Engine does the thinking for you.

To tick off what goodies Google App Engine does offer us:

  1. Nice error reporting
  2. Simplifies API security
  3. Reliability and support
  4. Usage quotas for free applications

Please, feel free to ask if you have any questions or comments in the comment section.

Author image
I am Chidume Nnamdi, a software dev and tech. writer from Nigeria.