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:
- Cloud Functions for the REST API of our web application
- API Gateway to handle API requests
- and IBM Cloud Object Storage to host the React.js app.
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.
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 pathresp:
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:
You can see the different endpoints we deployed by going to Functions > Actions
and choosing the project from the list.
You should see the different actions we deployed.
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:
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.
We can now create a bucket by clicking on the Create Bucket
button:
and on the arrow on the Customize you bucket
card:
Enter a unique name for the bucket:
Click on the Add rule
link on the Static website hosting
card:
enable Public access
:
and save:
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.
You should have the following files in your 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
and copying the last URL at the bottom of the page
Here is the result:
Credits:
- The original project: https://github.com/harounchebbi/Mern-Stack-Crud-App
- IBM article about hosting a static website: https://cloud.ibm.com/docs/cloud-object-storage?topic=cloud-object-storage-static-website-tutorial&programming_language=Console
- Original Cover photo by CHUTTERSNAP on Unsplash
#ibm, #ibm-cloud