Build server-side applications using microservices with Seneca.JS


Build server-side applications using microservices with Seneca.JS

Recipe ID: hsts-r49


Recipe Overview


The best way to understand Seneca and microservices architecture is by building
a server-side application that would benefit from the microservices architecture.
In previous articles, i- we learned about differences between monolithic versus microservices architecture and ii- we saw how large and complex server-side application benefits from the microservices architecture and why enterprises use microservices architecture. In this recipe, we will build a coupon website to practically demonstrate the benefits of using microservices architecture and Seneca to create a server-side application. While building this coupon site, you will also learn how to design a server-side application using the microservices architecture from scratch, how to split the functionality of the application into services, how a client can directly communicate with the services, and many other things.
Some of the things that we will cover in this recipe, apart from things related to Seneca and microservices architecture, are as follows:

 

Getting started

The coupon site that we will build will allow users to submit coupons. For the coupon to be publicly visible, the administrator of the site should accept the coupon. Every coupon will have an image attached to it that will be stored and served by an image storage server.
We will be using MongoDB to store the coupons. Before you continue further, make sure that you have MongoDB installed and running. I am assuming that you have basic knowledge of MongoDB.
The exercise files contain two directories: Initial and Final. Inside the Final directory, you will find the complete coupon site source code. In the Initial directory, you will find the HTML code and directories for the monolithic core, services, image storage server, and so on. You will put code related to them in their respective directories. The Initial directory will help you quickly get started with building the coupon site.
We won't get into designing the frontend of our coupon site. We will only be concentrating on building the architecture and functionalities of the site.
Therefore, the HTML code is already included in the Initial directory.

Architecture of our site

Our server-side application will be composed of a monolithic core, three services, MongoDB server, and image storage server.
The monolithic core will serve pages to the site visitors and administrators.
The three services are database service, URL configuration service, and upload
service. The following is what each of these services do:

We will be creating our own image storage server. However, in a production site, I would recommend that you use Amazon S3 or something similar to store images, as it makes it easy to serve images via CDN. You don't have to worry about scaling and reliability, and it's low cost. The image storage server that we will be creating will be a basic one to just demonstrate how to store images in a separate server and serve from there.
The following is the diagram that shows all the architecture's looks and how the servers in the architecture communicate with each other:
Microservices with Seneca

 

Creating the services

Let's first build the services before building the image storage server and monolithic core.
We will build the database service first, as it only depends on the MongoDB server, which is already running. The upload service and monolithic core depend on it, therefore it needs to be built before these.

Database service

The database service will provide actions to add coupons, list verified coupons, list unverified coupons, verify a coupon, and delete a coupon. These actions will be used by the upload service and monolithic core.
Open the Initial/database-service directory. Inside the directory, you will find a package.json file and an app.js file. The app.js file is where you will write the code, and package.json lists the dependencies for the database service. The database service is dependent on the seneca and seneca-mongo-store plugins.
Run the npm install command inside Initial/database-service to install the dependencies locally.
Here is the code to import the seneca module, create the seneca instance, attach the seneca-mongo-store plugin, and initialize the plugin to connect to MongoDB:
var seneca = require("seneca")();

seneca.use("mongo-store", { name: "gocoupons",
host: "127.0.0.1",
port: 27017
});
Here we are using gocoupons as the database name. I am assuming that the MongoDB server is running locally on the default port 27017.
The following is the code to create an action that allows you to add a coupon:
seneca.add({role: "coupons-store", cmd: "add"}, function(args, respond){
var coupons = seneca.make$("coupons");
var data = coupons.data$({title: args.title, desc: args.desc, email: args.email, url: args.url, price: args.price, discount: args.discount, thumbnail_id: args.thumbnail_id, verified: false});
data.save$(function(err, entity){

if(err) return respond(err);

respond(null, {value: true});
});
});
We will store the coupons in a collection named coupons. Here we are setting the verified property of the document to false, that is, whenever a new coupon is submitted by a user, we will make it unverified so that the administrator can retrieve this newly submitted coupon and verify it manually.
The thumbnail_id property doesn't hold the complete URL of the coupon
thumbnail, instead it's just the filename.
Here is the code to create an action to retrieve the verified coupons:
seneca.add({role: "coupons-store", cmd: "list"}, function(args, respond){
var coupons = seneca.make$("coupons"); coupons.list$({verified: true, limit$:21, skip$: args.skip},
function (err, entity){
if(err) return respond(err);

respond(null, entity);
})
});
This action retrieves maximum 21 coupons and it takes a skip argument that is used to skip some documents, making it possible to implement pagination using this action.
The following is the code to create an action to retrieve the unverified coupons:
seneca.add({role: "coupons-store", cmd: "admin_list"}, function(args, respond){
var coupons = seneca.make$("coupons"); coupons.list$({verified: false}, function (err, entity){
if(err) return respond(err);

respond(null, entity);
})
});
This action will be used to retrieve coupons to display on the admin panel for the administrator to accept or reject a coupon.
Here is the code to create an action to verify a coupon, that is, change the verified
property from false to true:
seneca.add({role: "coupons-store", cmd: "verified"}, function(args, respond){
var coupons = seneca.make$("coupons");
var data = coupons.data$({id: args.id, verified: true}); data.save$(function(err, entity){
if(err) return respond(error);

respond(null, {value: true});
});
});
This action will be invoked when the admin accepts a coupon to be displayed publicly.
Here is the code to create an action to delete a coupon:
seneca.add({role: "coupons-store", cmd: "delete"}, function(args, respond){
var coupons = seneca.make$("coupons"); coupons.remove$({id: args.id}); respond(null, {value: true});
});
This action will be invoked when the admin rejects a coupon.
Now that we have created all the actions for our database service, let's expose these actions via the network so that the other servers can call them. Here is the code to do this:
seneca.listen({port: "5010", pin: {role: "coupons-store"}});
Now go ahead and run the database service using the node app.js command.

URL config service

The upload services use the URL config service to find the base URL of the monolithic core so that it can redirect the user there once the coupon is submitted successfully. Also, the monolithic core uses this service to find the base URL of the image storage server and upload service so that it can include them in the HTML code.
Open the Initial/config-service directory. Inside the directory, you will find a package.json file and an app.js file. The app.js file is where you will write the code and package.json lists the dependencies for the config service. URL config service is only dependent on seneca. Run the npm install command inside Initial/config-service to install the dependencies locally.
The following is the code to import the seneca module and create actions to return the base URLs of the upload service, monolithic core, and image storage server:
var seneca = require("seneca")();
seneca.add({role: "url-config", cmd: "upload-service"}, function(args, respond){
respond(null, {value: "http://localhost:9090"});
});

seneca.add({role: "url-config", cmd: "monolithic-core"}, function(args, respond){
respond(null, {value: "http://localhost:8080"});
});

seneca.add({role: "url-config", cmd: "image-storage-service"}, function(args, respond){
respond(null, {value: "http://localhost:7070"});
});

seneca.listen({port: "5020", pin: {role: "url-config"}});
Now go ahead and run the URL config service using the node app.js command.

Upload service

The upload service handles the new coupon form submission. The form consists of  a coupon title, URL, description, price, discount price, and a thumbnail. The content type of form submission is multipart/form-data, as it is uploading an image file.
Open the Initial/upload-service directory. Inside the directory, you will find a package.json file and an app.js file. The app.js file is where you will write the code and package.json lists the dependencies for the upload service. The upload service is dependent on seneca, express, connect-multiparty, path, fs and request packages. Run the npm install command inside Initial/upload- service to install the dependencies locally.
The following is the code to import the modules:
var seneca = require("seneca")(); var app = require("express")();
var multipart = require("connect-multiparty")(); var path = require("path");
var fs = require("fs");
var request = require("request");
There are chances that the users may upload images with the same name. We don't want images with the same name to overwrite each other. Therefore, we need rename every image with a unique name. The following is the code for defining a function to generate a unique number, which will be used as an image name:
function uniqueNumber() { var date = Date.now();

if (date <= uniqueNumber.previous) { date = ++uniqueNumber.previous;
} else {
uniqueNumber.previous = date;
}

return date;
}

uniqueNumber.previous = 0;

function ID(){
return uniqueNumber();
};
Now, for the upload service to be able to communicate with the database and URL config services, we need to add them to the upload service seneca instance. The following is the code to do this:
seneca.client({port: "5020", pin: {role: "url-config"}});
seneca.client({port: "5010", pin: {role: "coupons-store"}});

Now we need to define an express route to handle POST requests submitted to the
/submit path. Inside the route handler, we will rename the image, upload the image to image storage server, add the metadata of the coupon to MongoDB using the database service, and redirect to the monolithic core with the status stating that the form was submitted successfully. Here is the code to define the route:
//declare route and add callbacks
app.post('/submit', multipart, function(httpRequest, httpResponse, next){

var tmp_path = httpRequest.files.thumbnail.path; var thumbnail_extension = path.extname(tmp_path); var thumbnail_directory = path.dirname(tmp_path); var thumbnail_id = ID();
var renamed_path = thumbnail_directory + '/' + ID() + thumbnail_ extension;

//rename file
fs.rename(tmp_path, renamed_path, function(err) {
if(err) return httpResponse.status(500).send("An error occured");

//upload file to image storage server
seneca.act({role: "url-config", cmd: "image-storage-service"}, function(err, storage_server_url){
var req = request.post(storage_server_url.value + "/store", function (err, resp, body){
fs.unlink(renamed_path);

if(err) return httpResponse.status(500).send("An error occured");

if(body == "Done")
{
//store the coupon
seneca.act({role: "coupons-store", cmd: "add", title: httpRequest.body.title, email: httpRequest.body.email, url: httpRequest.body.url, desc: httpRequest.body.desc, price: httpRequest. body.price, discount: httpRequest.body.price, thumbnail_id: thumbnail_ id + thumbnail_extension}, function(err, response){
if(err)
{
//delete the stored image
request.get(storage_server_url + "/delete/" + thumbnail_ id + thumbnail_extension);
httpResponse.status(500).send("An error occured");

return;
}
seneca.act({role: "url-config", cmd: "monolithic-core"}, function(err, response){
if(err) return httpResponse.status(500).send("An error
occured");

//redirect to monolithic core httpResponse.redirect(response.value +
"/?status=submitted");
});
});
}
});

var form = req.form();
form.append("thumbnail", fs.createReadStream(renamed_path)); form.append("name", thumbnail_id + thumbnail_extension);
});
});
});
Here is how the preceding code works:

So what you need to keep in mind while coding such services is that you need to handle all sorts of failures and also roll back changes if a failure occurs. Now, this also makes it easy to update and redeploy the database service, URL config service, and image storage server as the upload service handles the failure of these services and provides a feedback to the user.
Now we have defined our routes. Finally, we need to start the Express server.
The following is the code to do so:
app.listen(9090);
Now go ahead and run the upload service using the node app.js command.

Creating the image upload server

We have finished building the services. Now let's build the image storage server. The image storage server defines the routes using which an image can be stored, deleted, or retrieved.
Open the Initial/image-storage directory. Inside the directory, you will find a package.json file and an app.js file. The app.js file is where you will write the code, and package.json lists the dependencies for the image storage server. The upload service is dependent on express, connect-multiparty, path, and fs.
Run the npm install command inside Initial/image-storage to install the dependencies locally.
The following is the code to import the modules:
var express = require("express"); var app = express();
var fs = require("fs");
var multipart = require("connect-multiparty")();

Now let's define the route using which the upload service can store images in the image storage server. The upload service makes the POST request to the /store URL path to store the image. Here is the code to define the route:
app.post("/store", multipart, function(httpRequest, httpResponse, next){
var tmp_path = httpRequest.files.thumbnail.path;
var target_path = "public/images/" + httpRequest.body.name; fs.rename(tmp_path, target_path, function(err) {
if(err) return httpResponse.status(500).send("An error occured");

httpResponse.send("Done");
});
});
Here, at first, we are adding the callback provided by the connect-multiparty module, which parses the multipart/form-data content type body and also moves the files to a temporary location.
Then, we are moving the file from temporary directory to another directory. The directory we are moving the file to is public/images/. We are moving the file using the rename method of the filesystem module. Finally, we are sending a Done string as the body of HTTP response to tell the upload service that the file is stored successfully.
Now let's define the route using which the upload service can delete an image stored in the image storage server. The upload service makes the GET request to the /delete/:id URL path, where the id parameter indicates the image name. The following is the code to define the route:
app.get("/delete/:id", function(httpRequest, httpResponse, next){ fs.unlink("public/images/" + httpRequest.params.id, function(err) {
if(err) return httpResponse.status(500).send("An error occured");

httpResponse.send("Done");
});
});
Here we are deleting the image file using the unlink method of the fs module.

Finally, we need to serve images to the browser. Looking for static file in the public/ images/ directory can do this. The following is the code to do this:
app.use(express.static(  dirname + "/public/images"));
Here we are using the static middleware that looks for static files in the directory
provided by arguments and serves directly to the browser.
Now we have defined our routes. Finally, we need to start the Express server. Here is
the code to do so:
app.listen(9090);
Now go ahead and run the image storage server using the node app.js command.

Creating the monolithic core

We have finished creating the services and image storage server. The users interact with the monolithic core to view coupons and the admin interacts with the monolithic core to view unverified coupons, and then it either rejects or accepts a coupon. Other than new coupon submission by the user, everything else by the user and admin is done in the monolithic core.
Open the Initial/monolithic directory. Inside the directory, you will find a package.json file and an app.js file. The app.js file is where you will write the code, and package.json lists the dependencies for the monolithic core. The monolithic core is dependent on express, seneca, request and basic-auth npm packages. Run the npm install command inside Initial/monolithic to install the dependencies locally.
We will use the ejs template engine with Express. Inside the views directory, you will find ejs files for home, new coupon submit forms, and admin pages. The files already contain the templates and HTML code. The site is designed using Bootstrap.
The following is the code to import the modules:
var seneca = require("seneca")(); var express = require("express"); var app = express();
var basicAuth = require("basic-auth"); var request = require("request");

Now, for the monolithic core to be able to communicate with the database and
url- config services, we need to add them to the monolithic core seneca instance. The following is the code to do this:
seneca.client({port: "5020", pin: {role: "url-config"}});
seneca.client({port: "5010", pin: {role: "coupons-store"}});
Now we need to set ejs as the view engine. Here is the code to set ejs as the view engine:
app.set("view engine", "ejs");
All the static files such as CSS, JS, and fonts are kept on the public directory.
We need to serve them to the client. Here is the code to serve the static files:
app.use(express.static( dirname + "/public"));
Here we are serving the static files in the same way as we served the static files
(that is, images) in the image upload server.
Now we need to add a route to the server of the home page of our website that displays the first 20 coupons. It also displays the Next and Previous buttons to navigate between the next or previous 20 buttons.
The home page is accessed via the root URL. The following is the code to add a route to the server of the home page:
app.get("/", function(httpRequest, httpResponse, next){ if(httpRequest.query.status == "submitted") {
seneca.act({role: "coupons-store", cmd: "list", skip: 0}, function(err, coupons){
if(err) return httpResponse.status(500).send("An error occured");

seneca.act({role: "url-config", cmd: "image-storage- service"}, function(err, image_url){
if(err) return httpResponse.status(500).send("An error occured");

if(coupons.length > 20)
{
var next = true;
}
else
{

 

var next = false;
}

var prev = false;

httpResponse.render("index", {prev: prev, next: next, current: 0, coupons: coupons, image_url: image_url.value, submitted: true});
})
})

return;
};

if(parseInt(httpRequest.query.current) !== undefined && httpRequest.query.next == "true")
{
seneca.act({role: "coupons-store", cmd: "list", skip: parseInt(httpRequest.query.current) + 20}, function(err, coupons){
if(err) return httpResponse.status(500).send("An error occured");

seneca.act({role: "url-config", cmd: "image-storage- service"}, function(err, image_url){
if(err) return httpResponse.status(500).send("An error occured");

if(coupons.length > 20)
{
var next = true;
}
else
{
var next = false;
}

var prev = true;

httpResponse.render("index", {prev: prev, next: next, current: parseInt(httpRequest.query.current) + 20, coupons: coupons, image_url: image_url.value});
})
})
}

else if(parseInt(httpRequest.query.current) != undefined && httpRequest.query.prev == "true")
{
seneca.act({role: "coupons-store", cmd: "list", skip: parseInt(httpRequest.query.current) - 20}, function(err, coupons){
if(err) return httpResponse.status(500).send("An error occured");

seneca.act({role: "url-config", cmd: "image-storage- service"}, function(err, image_url){
if(err) return httpResponse.status(500).send("An error occured");

if(coupons.length > 20)
{
var next = true;
}
else
{
var next = false;
}

if(parseInt(httpRequest.query.current) <= 20)
{
var prev = false;
}
else
{
prev = true;
}

httpResponse.render("index", {prev: prev, next: next, current: parseInt(httpRequest.query.current) - 20, coupons: coupons, image_url: image_url.value});
})
})
}
else
{
seneca.act({role: "coupons-store", cmd: "list", skip: 0}, function(err, coupons){
if(err) return httpResponse.status(500).send("An error occured");

seneca.act({role: "url-config", cmd: "image-storage- service"}, function(err, image_url){
if(err) return httpResponse.status(500).send("An error occured");

if(coupons.length > 20)
{
var next = true;
}
else
{
var next = false;
}

var prev = false;

}
});


httpResponse.render("index", {prev: prev, next: next, current: 0, coupons: coupons, image_url: image_url.value});
})
})


The index.ejs file is the view of the home page of our site. The preceding code renders this view to generate the final HTML code for the home page.
The preceding code implements pagination by checking whether prev or next keys are present in the query string. If these keys are undefined, then it displays the first 20 coupons, otherwise it calculates the skip value argument by adding 20 to the value of the current key in the query string.
Then, the code checks whether the total number of coupons retrieved is 21 or less. If they are less than 21, then it doesn't display the Next button by assigning the next variable to false, otherwise it displays the next button by assigning the
next variable to true. However, the total number of coupons it displays is 20. We
retrieved an extra coupon to just check whether we should display the next button or not. To find out whether we should display the previous button or not is fairly easy, that is, if the next key is true in the query string, then we must display the previous button.

The preceding code also checks for the status=submitted query string that indicates the user was redirected back from the upload service. If it's present, then it assigns the submitted local variable for the view to true. This is the ejs template present in the view that checks whether the submitted local variable is true or undefined and displays a successful form submission message:
<% if(typeof submitted !== "undefined"){ %>
<% if(submitted == true){ %>
<div class="alert alert-success" role="alert">Coupon has been submitted. Our administrator will review and the coupon shortly.</div>
<% } %>
<% } %>
Here is the ejs template present in the view that displays the coupons and the next
and previous buttons:
<% if(coupons.length < 21){ %>
<% var cut = 0; %>
<% } %>
<% if(coupons.length == 21){ %>
<% var cut = 1; %>
<% } %>
<% for(var i = 0; i < coupons.length - cut; i++) {%>
<div class="col-sm-3 col-lg-3 col-md-3">
<div class="thumbnail">
<img src="<%= image_url + '/' + coupons[i].thumbnail_id %>" alt="">
<div class="caption">
<h4 class="pull-right"><del><%= coupons[i].price %></del>
<%= coupons[i].discount %></h4>
<h4><a href="<%= coupons[i].url %>"><%= coupons[i].title
%></a>
</h4>
<p><%= coupons[i].desc %></p>
</div>
</div>
</div>
<% } %>
</div>

<ul class="pager">
<% if(prev == true){ %>
<li class="previous"><a href="/?prev=true&current=<%= current
%>">Previous</a></li>
<% } %>
<% if(next == true){ %>
<li class="next"><a href="/?next=true&current=<%= current %>">Next</ a></li>
<% } %>
</ul>
We are done creating our home page. Now we need to create a route with the /add URL path that will display a form to submit a new coupon. The view for this coupon submission page is add.ejs. Here is the code to create the route:
app.get("/add", function(httpRequest, httpResponse, next){ seneca.act({role: "url-config", cmd: "upload-service"}, function(err, response){
if(err) return httpResponse.status(500).send("An error occured");

httpResponse.render("add", {upload_service_url: response.value});
})
});
Here we are retrieving the base URL of the upload service from the URL config service and assigning it to the upload_service_url local variable so that the form knows where to submit the POST request.
The following is the template in the add.ejs view that displays the coupon submission form:
<form role="form" method="post" action="<%= upload_service_url %>/ submit" enctype="multipart/form-data">
<div class="form-group">
<label for="email">Your Email address:</label>
<input type="email" class="form-control" id="email" name="email">
</div>
<div class="form-group">
<label for="title">Product Title:</label>
<input type="text" class="form-control" id="title" name="title">
</div>
<div class="form-group">
<label for="desc">Product Description:</label>

<textarea class="form-control" id="desc" name="desc"></textarea>
</div>
<div class="form-group">
<label for="url">Product URL: </label>
<input type="text" class="form-control" id="url" name="url">
</div>
<div class="form-group">
<label for="price">Original Price:</label>
<input type="text" class="form-control" id="price" name="price">
</div>
<div class="form-group">
<label for="discount">Discount Price:</label>
<input type="text" class="form-control" id="discount" name="discount">
</div>
<div class="form-group">
<label for="thumbnail">Product Image: <i>(320 x 150)</i></label>
<input type="file" class="form-control" id="thumbnail" name="thumbnail">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
Now we need to provide a path for the site admin to access the admin panel. The path to access admin panel is going to be /admin. The admin panel will be protected using HTTP basic authentication.
We will create two more routes that will be used by the admin to accept or reject a coupon. The routes are /admin/accept and /admin/reject.
The following is the code to protect the admin panel using the HTTP basic authentication:
var auth = function (req, res, next){ var user = basicAuth(req);

if (!user || !user.name || !user.pass)
{
res.set("WWW-Authenticate", "Basic realm=Authorization Required");
res.sendStatus(401);
}

//check username and password
if (user.name === "narayan" && user.pass === "mypassword")
{
next();
}
else
{
res.set("WWW-Authenticate", "Basic realm=Authorization Required");
res.sendStatus(401);
}
}

app.all("/admin/*", auth); app.all("/admin", auth);
Here we are executing the auth callback for all the admin panel paths. The callback checks whether the user is logged in or not. If user is not logged in, we will ask the user to log in. If the user tries to log in, then we will check whether the username and password is correct. If the username and password are wrong, we will ask the user to log in again. We will parse the HTTP basic authentication based the headers using the basic-auth module, that is, we will pass the req object to the basicAuth function to parse it. Here we are hardcoding the username and password.
Now we need to define the routes to access the admin panel. The admin.ejs file is
the view for the admin panel. The following is the code to add the routes:
app.get("/admin", function(httpRequest, httpResponse, next){ seneca.act({role: "coupons-store", cmd: "admin_list", skip: 0}, function(err, coupons){
if(err) return httpResponse.status(500).send("An error occured");

seneca.act({role: "url-config", cmd: "image-storage-service"}, function(err, image_url){
httpResponse.render("admin", {coupons: coupons, image_url: image_url.value});
});
});
});

app.get("/admin/accept", function(httpRequest, httpResponse, next){
seneca.act({role: "coupons-store", cmd: "verified", id: httpRequest.query.id}, function(err, verified){

if(err) return httpResponse.status(500).send("An error occured");

if(verified.value == true)
{
httpResponse.redirect("/admin");
}
else
{
httpResponse.status(500).send("An error occured");
}
});
});

app.get("/admin/reject", function(httpRequest, httpResponse, next){
seneca.act({role: "url-config", cmd: "image-storage-service"}, function(err, storage_server_url){
if(err) return httpResponse.status(500).send("An error occured");

request.get(storage_server_url.value + "/delete/" + httpRequest.query.thumbnail_id, function(err, resp, body){
if(err) return httpResponse.status(500).send("An error occured");

seneca.act({role: "coupons-store", cmd: "delete", id: httpRequest.query.id}, function(err, deleted){
if(err) return httpResponse.status(500).send("An error occured");

if(deleted.value == true)
{
httpResponse.redirect("/admin");
}
else
{
httpResponse.status(500).send("An error occured");
}
});
});
})
});

When the admin visits /admin, unverified coupons are displayed along with   buttons to accept or reject a coupon. When the admin clicks on the Accept button, then a request is made to the /admin/accept path to mark the coupon as verified, and when the admin clicks on the Reject button, a request is made to the /admin/ reject path to delete the coupon. After accepting or deleting a coupon, the admin is redirected to the /admin path.
The following is the template that displays the coupons and verification buttons to the admin:
<% for(var i = 0; i < coupons.length; i++) {%>
<tr>
<td><%= coupons[i].title %></td>
<td><%= coupons[i].desc %></td>
<td><%= coupons[i].url %></td>
<td><img style="width: 300px !important" src="<%= image_url + '/' + coupons[i].thumbnail_id %>" alt=""></td>
<td><%= coupons[i].price %></td>
<td><%= coupons[i].discount %></td>
<td>
<form role="form" method="get" action="/admin/accept">
<div class="form-group">
<input type="hidden" value="<%= coupons[i].id %>" name="id">
<input type="hidden" value="<%= coupons[i].thumbnail_id
%>" name="thumbnail_id">
<input type="submit" value="Accept" class="btn btn- default">
</div>
</form>
</td>
<td>
<form role="form" method="get" action="/admin/reject">
<div class="form-group">
<input type="hidden" value="<%= coupons[i].id %>" name="id">
<input type="hidden" value="<%= coupons[i].thumbnail_id%>" name="thumbnail_id">

<input type="submit" value="Reject" class="btn btn- default">
</div>
</form>
</td>
</tr>
<% } %>
We have defined our routes. Finally, we need to start the Express server. Here is the
code to do so:
app.listen(9090);
Now go ahead and run the monolithic core server using the node app.js command.

Website walkthrough

We have completed creating our website. Now, let's walkthrough our site to see how it works overall. Before that, make sure that everything is running.
You can visit the home page of the website using the http://localhost:8080/ URL. The following is how the web page will look when you will visit it for the first time:
Microservices with Seneca

Now to add a coupon, click on the Submit Coupon button. Now you will see a form. Fill in the form. Here is how it looks:Microservices with Seneca

 

Now submit the form. After submitting the form, you will be redirected to the home page. The following is how the home page will look after redirect:
Microservices with Seneca

Now click on the Admin button to visit the admin panel and accept the coupon. Here is how the admin panel will look:
Microservices with Seneca

Click on the Accept button to accept it. Now go back to the home page. This is how the home page will look now:
Microservices with Seneca

In the preceding image, you can see that the product is listed.

Further improvements to the site

Here is a list of things we can do now to make the site architecture even better and add some extra features. You will also get some practice writing code involving the microservices architecture by performing the following actions:

new service.

These are just some ideas to make the site even better.

Summary

In this article, we saw how to build a website using Seneca and microservices architecture from scratch. The website we built was simple in terms of features, but involved a lot of important techniques that are used while building sites using the microservices architecture. Now you are ready to choose the architecture that suits your site best. I also mentioned the things you can do to make the site even better.

Here are related articles if you wish to learn more advance topics for web development:

Best practices for securing and scaling Node.JS applications
Comprehensive overview of Angular 2 architecture and features
How Bootstrap 4 extensible content containers or Cards work
Comprehensive guide for migration from monolithic to microservices architecture
Comprehensive overview of Bootstrap 4 features for user interface customizations
Intro to functional reactive programming for advance js web development
Using advance js and webrtc for cross browser communications in real time
Intro to real-time bidirectional communications between browsers and webSocket servers

Junior or senior web developers can also explore career opportunities around blockchain development by reading below articles:

Blockchain Developer Guide- Comprehensive Blockchain Ethereum Developer Guide from Beginner to Advance Level
Blockchain Developer Guide- Comprehensive Blockchain Hyperledger Developer Guide from Beginner to Advance Level

Here are more hands-on recipes for advance web development:
Build advance single page application with Angular and Bootstrap
Develop microservices with monolithic core via advance Node.JS
Advance UI development with JS MVC framework and react
Develop advance JavaScript applications with functional reactive programming
Develop advance webcam site using Peerjs and Peerserver with Express.JS
Develop advance bidirectional communication site with websockets and Express.JS

This tutorial is developed by Narayan Prusty who is our senior Blockchain instructor.

 

Related Training Courses

Hands-on Node.JS, MongoDB and Express.js Training
Advance JavaScript, jQuery Using JSON and Ajax
Developing Web Applications Using Angular.JS
Design websites with JavaScript React in 30 hours
Blockchain Certified Solution Architect in 30 hours
Advance JavaScript, jQuery Using JSON and Ajax
Introduction to Python Programming
Object Oriented Programming with UML


Private and Custom Tutoring

We provide private tutoring classes online and offline (at our DC site or your preferred location) with custom curriculum for almost all of our classes for $50 per hour online or $75 per hour in DC. Give us a call or submit our private tutoring registration form to discuss your needs.


View Other Classes!