Node Express and EJS - Webbprogrammering/websoft GitHub Wiki
We will build an application and web server using Node.js and the Express.js module.
The server should serve static files such as images, CSS and JavaScript (client side) along with dynamic routes managed by JavaScript on the server side together with Express.js and Node.js.
As a template engine, we use Embedded JavaScript templating (EJS) which helps us render HTML pages with dynamic JavaScript information. With a template engine, we can generate parts of the HTML code and print content from variables in JavaScript so that the content of the pages becomes dynamic.
Write all your code in work/s04/express
.
You can do the basics of Node.js and JavaScript on the server side.
It is good if you have basic knowledge of HTML and CSS (as well as JavaScript on the client side). These techniques are used in the article but are not discussed in detail.
The example programs used in the article can be found in your course rep database under example/nodejs/express
.
[Express] (https://expressjs.com/) is a web and application server for Node.js. Express is part of the [MEAN] concept (http://mean.io/) which is a collection of modules for building web applications with Node.js. In this article we will use Express.js (E) and Node.js (N) in MEAN.
The M stands for MongoDB which is a database and the A stands for Angular which is a JavaScript framework for building websites. We will not touch on these two parts.
When MEAN was introduced, the concept initially grew as a counterpart to LAMP/WAMP/MAMP/XAMPP used for web development according to Linux/Windows/MacOS, Apache, MariaDB/MySQL and PHP. The MEAN concept is currently not that strong, as its own brand, but its parts are highly relevant.
Go to an empty directory where you can build your sample code.
First we need to install the npm modules needed and before that a package.json
is required which can save information about the modules we will use.
# Go to the directory where you want to work
npm init
You can only press enter after each question, there is nothing we need to change.
You now have a file package.json
in your directory.
Now we can install the packages we will use. There are express
and ejs
.
npm install express ejs
The modules that are installed are saved in the package.json
file with details on which version was installed. You can open the file in your text editor and see what it contains.
Done. Then we test if the installation went well by building a server in Express.
Let's start up a server to see that the installation went well.
I start with code that boots the server together with a route for /
and en route for /about
and saves in the file index1.js
.
/**
* A sample Express server.
*/
"use strict";
// Enable server to run on port selected by the user selected
// environment variable DBWEBB_PORT
const port = process.env.DBWEBB_PORT || 1337;
// Set upp Express server
const express = require("express");
const app = express();
// This is middleware called for all routes.
// Middleware takes three parameters.
// Its callback ends with a call to next() to proceed to the next
// middleware, or the actual route.
app.use((req, res, next) => {
console.info(`Got request on ${req.path} (${req.method}).`);
next();
});
// Add a route for the path /
app.get("/", (req, res) => {
res.send("Hello World");
});
// Add a route for the path /about
app.get("/about", (req, res) => {
res.send("About something");
});
// Start up server and begin listen to requests
app.listen(port, () => {
console.info(`Server is listening on port ${port}.`);
// Show which routes are supported
console.info("Available routes are:");
app._router.stack.forEach((r) => {
if (r.route && r.route.path) {
console.info(r.route.path);
}
});
});
The code (found in your course repo under example/nodejs/express/index1.js
) prepares the Express server in the variable app
and adds two routes for the paths /
and /about
. They will each give their answer when we reach them via their path in the browser. We also add a middleware called whatever path, it prints incoming request details, good for debugging. At the end of the code, we start the server at the selected port and print details in the terminal of the server that was started.
This is how to start it through Node.js.
$ node index1.js
Server is listening on port 1337.
Available routes are:
/
/about
Now the server listens to the selected port and I can open a browser against its routes.
First towards the index route /
.
The towards the route /about
.
We can also try to reach a route that does not exist, for example /error
. It should display an error message.
On the server side, every request is printed by the middleware we created.
Got request on / (GET).
Got request on /about (GET).
Got request on /error (GET).
Well, we have an Express server up and running and responding to calls on a couple of routes. A good start.
Before we proceed, we restructure the code. As the code grows, we want to use modules, files and functions to get a good code structure that is easy to maintain and build upon.
I start by making a copy of index1.js
and save as index2.js
. You can find the code example in example/nodejs/express/index2.js
.
We want to highlight the functions that are callbacks for each route, to a separate file route/index.js
.
As there are more and more routes, it is good to gather the routes that belong together, in our own files in a subdirectory that we name as route
.
In the file route/index.js
I put the following code.
/**
* General routes.
*/
"use strict";
var express = require('express');
var router = express.Router();
// Add a route for the path /
router.get('/', (req, res) => {
res.send("Hello World");
});
// Add a route for the path /about
router.get("/about", (req, res) => {
res.send("About something");
});
module.exports = router;
In the code we build an Express router with the routes we want, at the end of the file we export router
.
Now we can update index2.js
and make require on the router module. Then we use app.use()
to mount the entire router into the server, with all its routes.
const routeIndex = require("./route/index.js");
app.use("/", routeIndex);
We can say that we mount routes from the route/index.js
module to the /
mount point.
If we restart the server, we can see that the two /
and /about
routers work as before.
We have a good structure for routes, it helps us when more and more routes want to enter our server.
We do the corresponding update for our middleware and put a custom file in middleware/index.js
. Who knows, we may want to build more middleware in the future.
In middleware/index.js
we put the following code.
/**
* General middleware.
*/
"use strict";
/**
* Log incoming requests to console to see who accesses the server
* on what route.
*
* @param {Request} req The incoming request.
* @param {Response} res The outgoing response.
* @param {Function} next Next to call in chain of middleware.
*
* @returns {void}
*/
function logIncomingToConsole(req, res, next) {
console.info(`Got request on ${req.path} (${req.method}).`);
next();
}
module.exports = {
logIncomingToConsole: logIncomingToConsole
};
We organize our middleware as a function and at the end of the file we choose to export the function.
Now we can import the module and use the exported function in our server like this.
const middleware = require("./middleware/index.js");
app.use(middleware.logIncomingToConsole);
There is less code in index2.js
and more clear. It is good when the code base is growing.
The term middleware is common in this context, it is code that is supposed to run before calling one or more route callbacks. This can, for example, mean logging or verifying that a user is logged in and has the right to execute the callback that is called.
Our code for printing which routes are available needs to be updated as we now use an Express Router. In conjunction with this, we put it in a separate function in the file index2.js
along with the rest of the code that starts the server.
The updated feature looks like this.
/**
* Log app details to console when starting up.
*
* @return {void}
*/
function logStartUpDetailsToConsole() {
let routes = [];
// Find what routes are supported
app._router.stack.forEach((middleware) => {
if (middleware.route){
// Routes registered directly on the app
routes.push(middleware.route);
} else if(middleware.name === "router") {
// Routes added as router middleware
middleware.handle.stack.forEach((handler) => {
let route;
route = handler.route;
route && routes.push(route);
});
}
});
console.info(`Server is listening on port ${port}.`);
console.info("Available routes are:");
console.info(routes);
}
I print the entire data structure for routes
, it gets easier that way.
It can be useful to have such a function when you want to print what the server contains. It can help you debugging and developing the server.
Now the function can be used as a callback when app
starts.
app.listen(port, logStartUpDetailsToConsole);
The code that boots the server is now even more manageable.
What code is left at the top of the file index2.js
? Well this one.
/**
* A sample Express server.
*/
"use strict";
const port = process.env.DBWEBB_PORT || 1337;
const express = require("express");
const app = express();
const routeIndex = require("./route/index.js");
const middleware = require("./middleware/index.js");
app.use(middleware.logIncomingToConsole);
app.use("/", routeIndex);
app.listen(port, logStartUpDetailsToConsole);
// excluding function logStartUpDetailsToConsole by intention
We set a port, load the module for Express and initiate an app
that we add to midleware and routes and then boot up.
We start the server and test run it to see that everything works as before.
$ node index2.js
Server is listening on port 1337.
Available routes are:
[ Route { path: '/', stack: [ [Object] ], methods: { get: true } },
Route { path: '/about', stack: [ [Object] ], methods: { get: true } } ]
Got request on / (GET).
Got request on /about (GET).
Got request on /error (GET).
Fine, everything looks and works much like before, but we have a much better basic structure that we can expand upon.
In a website, static resources such as images, stylesheets and JavaScript files for the client side are common. You may also want HTML files that do not require any extra handling.
In Express, we can mount one (or more) directories and use them to deliver static resources to the browser.
I make a copy of index2.js
and rename it index3.js
and make changes to that file.
To try this out, you can make a copy of all the files in example/report
or work/report
and save them under a new directory public/
. This is a static website with only static resources, to complement the dynamic routes we have written previously.
Check that you have a file structure looking like this.
public/
βββ about.html
βββ css
β βββ style.css
βββ favicon.ico
βββ img
β βββ me.jpg
βββ js
β βββ main.js
βββ me.html
βββ report.html
3 directories, 7 files
Good, then we must add and configure, in Express, so that we access these resources through the browser.
The following code lines in index3.js
allow us to add a directory containing static resources.
const path = require("path");
app.use(express.static(path.join(__dirname, "public")));
We use the path
module from Node.js to create the path to the directory public
. The path is then mounted via app.use()
using express.static()
.
Now I can start the server and reach the me.html
page in the usual way.
I'll start the server.
$ node index3.js
Server is listening on port 1337.
Available routes are:
[ Route { path: '/', stack: [ [Object] ], methods: { get: true } },
Route { path: '/about', stack: [ [Object] ], methods: { get: true } } ]
I open the browser against /me.html
.
I opened up devtools in the browser to see what resources were loaded as part of the page. In addition to me.html
, also came the stylesheet style.css
, a client-JavaScript main.js
and an image me.jpg
.
When I look at the print on the server side, I see that it is these four resources that are executed when I load the page.
Got request on /me.html (GET).
Got request on /style/style.css (GET).
Got request on /js/main.js (GET).
Got request on /img/me.jpg (GET).
Well, we can now handle static resources in Express. It becomes much like a traditional web server.
Note that the order plays a role in index3.js
and how to add the various resources, static, middleware and routes, into app
. If you want the logging to take place then that resource must be added before the static resource.
If you add the static resource first, and the logging afterwards, the logging will never happen because the request ends with the static resource being delivered to the browser.
In my index3.js
the order is like this.
app.use(middleware.logIncomingToConsole);
app.use(express.static(path.join(__dirname, "public")));
app.use("/", routeIndex);
app.listen(port, logStartUpDetailsToConsole);
My middleware logger is located first and will always be used, regardless of which resource then returns the response to the browser and the user.
Let's look at how we can render responses that are a combination of HTML and JavaScript variables, we call it dynamic pages because they change the appearance depending on the variables content.
We use EJS as a template engine, their website supports us with the documentation. It's good to have now when we start using the template engine to create dynamic HTML pages.
I copy index3.js
and save as index4.js
and like my changes there.
Let's create the smallest possible setup to load a view file that prints today's date and time.
We need to tell Express that we want to use EJS as a template engine. Express supports many different template engines.
app.set("view engine", "ejs");
We need a view file, or template files that they can also be called.
Express assumes that the view files are in the views/
directory and are named with the file extension .ejs
.
I create views/today.ejs
as an example view and add the following code to the file. It is a mixture of HTML and code that the template engine interprets as variables from JavaScript.
<h1>Today is...</h1>
<p>The date and time is: <%= date %></p>
A template engine links variables from Node.js and JavaScript with static HTML, and allows us to generate pages of dynamic content, content that we can retrieve from, for example, a database or other source.
In the code above, the plain HTML code and code for the template engine <% = date%>
print the value of the variable date
.
But where does the variable date
come from?
We need an route that prepares the data to be sent to a view and renders the view itself with the data as input.
I make a new module for this route in route/today.js
using the following code.
/**
* Route for today.
*/
"use strict";
var express = require("express");
var router = express.Router();
router.get("/", (req, res) => {
let data = {};
data.date = new Date();
res.render("today", data);
});
module.exports = router;
I am preparing a data object that contains all the variables I want to send to the view. At the end of the route manager, I ask that the view "today"
, which corresponds to "views/today.ejs"
, should be rendered with the variables contained in data
.
As you can see, I use the /
route as a reference and it is in index4.js
that I choose where this route should be mounted on the server.
const routeToday = require("./route/today.js");
app.use("/today", routeToday);
I mount the route on /today
. This way gives me space to write collections of routes and the user of them can then decide exactly where to mount them on the application server.
When everything works together we can test the route and get the following page.
Now we can link code and values in JavaScript in Node.js with the web pages we render. We get dynamic web pages.
This was an introduction to getting started with the web and application server Express together router, middleware and template engine.
You now have the basics for writing your own web/application server.
Create an issue if you have questions or improvements to this article.