How to build and host a Node.js and React.js web application on IBM Cloud?

Hi! Welcome to this tutorial about building and hosting a Node.js and React.js web application on IBM Cloud. It will be a simple CRUD app to enroll, update, list, search, and delete students.

Assumption

This blog assumes that you have at least a basic understanding of Node.js, React.js, and YAML or you're just reading for fun :).

First of all, what is IBM Cloud?

IBM Cloud is a cloud platform that offers a set of cloud computing services for businesses. For this tutorial, I will use three (03) IBM Cloud services:

These services are paid, but fortunately, IBM Cloud offers a Free Tier that you can use without spending a penny. If you don't have an IBM Cloud account, you can register here.

1. Let's create our REST API with Cloud Functions

IBM Cloud functions are a functions-as-a-service (FaaS) programming platform for developing lightweight code that scalably executes on demand. You no more need to worry about managing servers and you just focus on development. I will show you how to create a serverless application using Node.js OpenWhisk template and deploy it to IBM Cloud along with configuring the API Gateway to get responses from this application.

First of all, we need to globally install the serverless npm package. Do so, by running (in your terminal of course):

npm install -g serverless

Let's create our project using the OpenWhisk template by running (in the project directory):

serverless create --template openwhisk-nodejs

The serverless package has generated a boilerplate for us. Let's open the project folder with our code editor, I will use Visual Studio Code. Here are the files created by the serverless package.

Files Generated by the serverless package The serverless.yml file is the configuration file for the serverless application. We also have a package.json file for handling npm packages and scripts and a handler.js file in which we're going to write our Rest API. Here is the code for our simple CRUD API, insert it in the handler.js file.

'use strict';
const mongoose = require('mongoose');

// Student model
const Students = require('./students');

// Replace by your own MongoDB URI
const mongodbURI = "mongodb+srv://kameon-api_0:KameonApi0@cluster0-xzqgj.mongodb.net/students-crud?retryWrites=true&w=majority";

mongoose.connect(mongodbURI, { useNewUrlParser: true });

// @route   GET /api/students/
// @desc    Get all students
// @access  Public
function getStudents() {
  return new Promise((reject, resolve) => {
    Students.find()
      .then(students => {
        resolve({ success: true,
          students: students.map(s => {
            return {
              id: s.id,
              name: s.name,
              email: s.email,
              enrollnumber: s.enrollnumber
            };
          })
        });
      })
      .catch(_err => {
        reject({
          success: false,
          error: 'Failed to fetch students from database!',
          status: 500
        });
      });
  });
}

// @route   GET /api/student?id=
// @desc    Get a specific student
// @access  Public
function getStudent(params) {
  return new Promise((resolve, reject) => {
    const id = params.id || false;
    if (id) {
      Students.findById(id)
        .then(student => {
          resolve({ success: true, student });
        })
        .catch(_err => {
          reject({
            success: false,
            statusCode: 404,
            message: 'Student not found!'
          });
        });
    } else {
      reject({
        success: false,
        error: 'The id field is required!',
        code: 400
      });
    }
  });
}

// @route   POST /api/students/
// @desc    Create a student
// @access  Public
function createStudent(params) {
  return new Promise((resolve, reject) => {
    const { name, email, enrollnumber } = params;
      if (name && email && enrollnumber) {
        Students.create({ name, email, enrollnumber })
        .then(student => {
          resolve({ success: true, student });
        })
        .catch(err => {
          reject({
            success: false,
            statusCode: 404,
            message: err
          });
        });
      } else {
        reject({
          success: false,
          error: 'All the fields are required!',
          code: 400
        });
      }
  });
}

// @route   PUT /api/students
// @desc    Update a student
// @access  Public
function updateStudent(params) {
  return new Promise((resolve, reject) => {
    const { id, name, email, enrollnumber } = params;
      if (id && name && email && enrollnumber) {
        Students.findByIdAndUpdate(id, { name, email, enrollnumber })
        .then(_doc => {
          resolve({ success: true, message: 'The student was updated'});
        })
        .catch(err => {
          reject({
            success: false,
            statusCode: 404,
            message: err
          });
        });
      } else {
        reject({
          success: false,
          error: 'All the fields are required!',
          code: 400
        });
      }
  });
}

// @route   DELETE /api/students?id=
// @desc    Delete a student
// @access  Public
function deleteStudent(params) {
  return new Promise((resolve, reject) => {
    const id = params.id || false;
    if (id) {
      Students.findByIdAndRemove(id)
        .then(_doc => {
          resolve({ success: true, message: 'The student was removed' });
        })
        .catch(err => {
          reject({
            success: false,
            statusCode: 400,
            message: err
          });
        });
    } else {
      reject({
        success: false,
        error: 'The id field is required!',
        code: 400
      });
    }
  });
}

exports.getstudents = getStudents;
exports.getstudent = getStudent;
exports.createstudent = createStudent;
exports.updatestudent = updateStudent;
exports.deletestudent = deleteStudent;

We also need to update the serverless.yml file; this is the new content of the file:

service: student-crud-app

provider:
  name: openwhisk

functions:
  getstudents:
    handler: handler.getstudents
    events:
      - http:
          method: get
          path: /students
          resp: json
  getstudent:
    handler: handler.getstudent
    events:
      - http:
          method: get
          path: /students/{id}
          resp: json
  createstudent:
    handler: handler.createstudent
    events:
      - http:
          method: post
          path: /students
          resp: json
  updatestudent:
    handler: handler.updatestudent
    events:
      - http:
          method: put
          path: /students/{id}
          resp: json
  deletestudent:
    handler: handler.deletestudent
    events:
      - http:
          method: delete
          path: /students/{id}
          resp: json

plugins:
  - serverless-openwhisk

resources:
  apigw:
    name: 'student-crud-api'
    basepath: /api
    cors: true

The functions: block is used to define the different API endpoints :

  • handler: the function that should be invoked,
  • events: the event that should trigger the function (an HTTP request for example)
  • method: the HTTP request method (GET, POST, DELETE, PUT, PATCH)
  • path: the endpoint path
  • resp: the response type sent by the invoked function.

In the resources: block, we define the API Gateway with apigw and :

  • set it's name with name:
  • set it's base path with basepath:
  • enable cors (to allow requests from any domain) with cors: true.

If you find the serveless.yml file hard to understand, please refer to the OpenWhisk - serverless.yml Reference;

The package.json file:

{
  "name": "student-crud-app",
  "version": "1.0.0",
  "description": "Sample OpenWhisk NodeJS serverless framework service.",
  "main": "handler.js",
  "keywords": [
    "serverless",
    "openwhisk"
  ],
  "devDependencies": {
    "serverless-openwhisk": ">=0.13.0"
  },
  "dependencies": {
    "mongoose": "^5.11.7"
  }
}

and the student model:

const mongoose = require('mongoose');

const studentSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    minlength: 3,
    maxlength: 33,
    trim: true
  },
  email: {
    type: String,
    required: true,
    trim: true
  },
  enrollnumber: {
    type: Number,
    min: 1,
    max: 120
  }
});

module.exports = mongoose.model('students', studentSchema);

To install the required dependencies, run:

npm install

Note that I'm using a MongoDB database, you can create a new one for free here. We can now deploy our REST API on IBM Cloud. But before that, we need to set up our IBM Cloud account, follow the detailed instructions here. To deploy the app, run:

serverless deploy

When deployed, you should see a similar output in your console: serverless deploy output You can see the different endpoints we deployed by going to Functions > Actions and choosing the project from the list.

Open Actions dashboard IBM Cloud You should see the different actions we deployed.

Deployed actions

2. Let's now host the React.js app with the Cloud Object Storage

The frontend source code is available on github. To follow along with me, clone the mentioned git repository and run npm install to install the required dependencies and then npm run build to build the app. You will obtain a build directory as no the following image: Build directory We need to create an instance of IBM Cloud Object Storage at this URL. On that page click on the Create button at the page bottom.

Create Button

We can now create a bucket by clicking on the Create Bucket button:

Create bucket button

and on the arrow on the Customize you bucket card:

Customize your bucket

Enter a unique name for the bucket:

Bucket name

Click on the Add rule link on the Static website hosting card:

Static website hosting card

enable Public access:

Enable Public access

and save:

Save configuration

Click on the Create bucket button at the bottom of the page to complete the bucket setup.

We can now upload the React build files by clicking on the Upload button.

Upload button

You should have the following files in your bucket:

Files in the bucket

The web application is now deployed and accessible here.

You should access yours at

https://<bucketname>.s3-web.<endpoint>/

mine is at

https://student-crud-app-0101.s3-web.eu-de.cloud-object-storage.appdomain.cloud/

You can get this URL by clicking on Configuration on the left side Configuratin button and copying the last URL at the bottom of the page direct access link

Here is the result:

image.png

Credits:

#ibm, #ibm-cloud

No Comments Yet