035.2 Lesson 2
Certificate: |
Web Development Essentials |
---|---|
Version: |
1.0 |
Topic: |
035 NodeJS Server Programming |
Objective: |
035.2 NodeJS Express Basics |
Lesson: |
2 of 2 |
Introduction
Web servers have very versatile mechanisms to produce responses to client requests. For some requests, it’s enough for the web server to provide a static, unprocessed reponse, because the requested resource is the same for any client. For instance, when a client requests an image that is accessible to everyone, it is enough for the server to send the file containing the image.
But when responses are dynamically generated, they may need to be better structured than simple lines written in the server script. In such cases, it is convenient for the web server to be able to generate a complete document, which can be interpreted and rendered by the client. In the context of web application development, HTML documents are commonly created as templates and kept separate from the server script, which inserts dynamic data in predetermined places in the appropriate template and then sends the formatted response to the client.
Web applications often consume both static and dynamic resources. An HTML document, even if it was dynamically generated, may have references to static resources such as CSS files and images. To demonstrate how Express helps handle this kind of demand, we’ll first set up an example server that delivers static files and then implement routes that generate structured, template-based responses.
Static Files
The first step is to create the JavaScript file that will run as a server. Let’s follow the same pattern covered in previous lessons to create a simple Express application: first create a directory called server
and then install the base components with the npm
command:
$ mkdir server $ cd server/ $ npm init $ npm install express
For the entry point, any filename can be used, but here we will use the default filename: index.js
. The following listing shows a basic index.js
file that will be used as a starting point for our server:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
You don’t have to write explicit code to send a static file. Express has middleware for this purpose, called express.static
. If your server needs to send static files to the client, just load the express.static
middleware at the beginning of the script:
app.use(express.static('public'))
The public
parameter indicates the directory that stores files the client can request. Paths requested by clients must not include the public
directory, but only the filename, or the path to the file relative to the public
directory. To request the public/layout.css
file, for example, the client makes a request to /layout.css
.
Formatted Output
While sending static content is straightforward, dynamically generated content can vary widely. Creating dynamic responses with short messages makes it easy to test applications in their initial stages of development. For example, the following is a test route that just sends back to the client a message that it sent by the HTTP POST
method. The response can just replicate the message content in plain text, without any formatting:
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
A route like this is a good example to use when learning Express and for diagnostics purposes, where a raw response sent with res.send()
is enough. But a useful server must be able to produce more complex responses. We’ll move on now to develop that more sophisticated type of route.
Our new application, instead of just sending back the contents of the current request, maintains a complete list of the messages sent in previous requests by each client and sends back each client’s list when requested. A response merging all messages is an option, but other formatted output modes are more appropriate, especially as responses become more elaborate.
To receive and store client messages sent during the current session, first we need to include extra modules for handling cookies and data sent via the HTTP POST
method. The only purpose of the following example server is to log messages sent via POST
and display previously sent messages when the client issues a GET
request. So there are two routes for the /
path. The first route fulfills requests made with the HTTP POST
method and the second fulfills requests made with the HTTP GET
method:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.use(express.static('public'))
const cookieParser = require('cookie-parser')
app.use(cookieParser())
const { v4: uuidv4 } = require('uuid')
app.use(express.urlencoded({ extended: true }))
// Array to store messages
let messages = []
app.post('/', (req, res) => {
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Locate cookie in the request
let uuid = req.cookies.uuid
// If there is no uuid cookie, create a new one
if ( uuid === undefined )
uuid = uuidv4()
// Add message first in the messages array
messages.unshift({uuid: uuid, message: req.body.message})
// Collect all previous messages for uuid
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
// Update cookie expiration date
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// Send back JSON response
res.json(user_entries)
})
app.get('/', (req, res) => {
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Locate cookie in the request
let uuid = req.cookies.uuid
// Client's own messages
let user_entries = []
// If there is no uuid cookie, create a new one
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
// Collect messages for uuid
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
// Update cookie expiration date
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// Send back JSON response
res.json(user_entries)
})
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
We kept the static files configuration at the top, because it will soon be useful to provide static files such as layout.css
. In addition to the cookie-parser
middleware introduced in the previous chapter, the example also includes the uuid
middleware to generate a unique identification number passed as a cookie to each client that sends a message. If not already installed in the example server directory, these modules can be installed with the command npm install cookie-parser uuid
.
The global array called messages
stores the messages sent by all clients. Each item in this array consists of an object with the properties uuid
and message
.
What’s really new in this script is the res.json()
method, used at the end of the two routes to generate a response in JSON format with the array containing the messages already sent by the client:
// Send back JSON response
res.json(user_entries)
JSON is a plain text format that allows you to group a set of data into a single structure that is associative: that is, content is expressed as keys and values. JSON is particularly useful when responses are going to be processed by the client. Using this format, a JavaScript object or array can be easily reconstructed on the client side with all the properties and indexes of the original object on the server.
Because we are structuring each message in JSON, we refuse requests that do not contain application/json
in their accept
header:
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
A request made with a plain curl
command to insert a new message will not be accepted, because curl
by default does not specify application/json
in the accept
header:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt Not Found
The -H "accept: application/json"
option changes the request header to specify the response’s format, which this time will be accepted and answered in the specified format:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt -H "accept: application/json" ["My first message"]
Getting messages using the other route is done in a similar way, but this time using the HTTP GET
method:
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -H "accept: application/json" ["Another message","My first message"]
Templates
Responses in formats such as JSON are convenient for communicating between programs, but the main purpose of most web application servers is to produce HTML content for human consumption. Embedding HTML code within JavaScript code is not a good idea, because mixing languages in the same file makes the program more susceptible to errors and harms the maintenance of the code.
Express can work with different template engines that separate out the HTML for dynamic content; the full list can be found at the Express template engines site. One of the most popular template engines is Embedded JavaScript (EJS), which allows you to create HTML files with specific tags for dynamic content insertion.
Like other Express components, EJS needs to be installed in the directory where the server is running:
$ npm install ejs
Next, the EJS engine must be set as the default renderer in the server script (near the beginning of the index.js
file, before the route definitions):
app.set('view engine', 'ejs')
The response generated with the template is sent to the client with the res.render()
function, which receives as parameters the template file name and an object containing values that will be accessible from within the template. The routes used in the previous example can be rewritten to generate HTML responses as well as JSON:
app.post('/', (req, res) => {
let uuid = req.cookies.uuid
if ( uuid === undefined )
uuid = uuidv4()
messages.unshift({uuid: uuid, message: req.body.message})
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
})
app.get('/', (req, res) => {
let uuid = req.cookies.uuid
let user_entries = []
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
})
Note that the format of the response depends on the accept
header found in the request:
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
A response in JSON format is sent only if the client explicitly requests it. Otherwise, the response is generated from the index
template. The same user_entries
array feeds both the JSON output and the template, but the object used as a parameter for the latter also has the title: "My messages"
property, which will be used as a title inside the template.
HTML Templates
Like static files, the files containing HTML templates reside in their own directory. By default, EJS assumes the template files are in the views/
directory. In the example, a template named index
was used, so EJS looks for the views/index.ejs
file. The following listing is the content of a simple views/index.ejs
template that can be used with the example code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><%= title %></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/layout.css">
</head>
<body>
<div id="interface">
<form action="/" method="post">
<p>
<input type="text" name="message">
<input type="submit" value="Submit">
</p>
</form>
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
</div>
</body>
</html>
The first special EJS tag is the <title>
element in the <head>
section:
<%= title %>
During the rendering process, this special tag will be replaced by the value of the title
property of the object passed as a parameter to the res.render()
function.
Most of the template is made up of conventional HTML code, so the template contains the HTML form for sending new messages. The test server responds to the HTTP GET
and POST
methods for the same path /
, hence the action="/"
and method="post"
attributes in the form tag.
Other parts of the template are a mixture of HTML code and EJS tags. EJS has tags for specific purposes within the template:
<% … %>
-
Inserts flow control. No content is directly inserted by this tag, but it can be used with JavaScript structures to choose, repeat, or suppress sections of HTML. Example starting a loop:
<% messages.forEach( (message) => { %>
<%# … %>
-
Defines a comment, whose content is ignored by the parser. Unlike comments written in HTML, these comments are not visible to the client.
<%= … %>
-
Inserts the escaped content of the variable. It is important to escape unknown content to avoid JavaScript code execution, which can open loopholes for cross-site Scripting (XSS) attacks. Example:
<%= title %>
<%- … %>
-
Inserts the content of the variable without escaping.
The mix of HTML code and EJS tags is evident in the snippet where client messages are rendered as an HTML list:
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
In this snippet, the first <% … %>
tag starts a forEach
statement that loops through all the elements of the message
array. The <%
and %>
delimiters let you control the snippets of HTML. A new HTML list item, <li><%= message %></li>
, will be produced for each element of messages
. With these changes, the server will send the response in HTML when a request like the following is received:
$ curl http://myserver:8080/ --data message="This time" -c cookies.txt -b cookies.txt <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>My messages</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/layout.css"> </head> <body> <div id="interface"> <form action="/" method="post"> <p> <input type="text" name="message"> <input type="submit" value="Submit"> </p> </form> <ul> <li>This time</li> <li>in HTML</li> </ul> </div> </body> </html>
The separation between the code for processing the requests and the code for presenting the response makes the code cleaner and allows a team to divide application development between people with distinct specialties. A web designer, for example, can focus on the template files in views/
and related stylesheets, which are provided as static files stored in the public/
directory on the example server.
Guided Exercises
-
How should
express.static
be configured so that clients can request files in theassets
directory? -
How can the response’s type, which is specified in the request’s header, be identified within an Express route?
-
Which method of the
res
(response) route parameter generates a response in JSON format from a JavaScript array calledcontent
?
Explorational Exercises
-
By default, Express template files are in the
views
directory. How can this setting be modified so that template files are stored intemplates
? -
Suppose a client receives an HTML response with no title (i.e.
<title></title>
). After verifying the EJS template, the developer finds the<title><% title %></title>
tag in thehead
section of the file. What is the likely cause of the problem? -
Use EJS template tags to write a
<h2></h2>
HTML tag with the contents of the JavaScript variableh2
. This tag should be rendered only if theh2
variable is not empty.
Summary
This lesson covers the basic methods Express.js provides to generate static and formated yet dynamic responses. Little effort is required to set up an HTTP server for static files and the EJS templating system provides an easy way for generating dynamic content from HTML files. This lesson goes through the following concepts and procedures:
-
Using
express.static
for static file responses. -
How to create a response to match the content type field in the request header.
-
JSON-structured responses.
-
Using EJS tags in HTML based templates.
Answers to Guided Exercises
-
How should
express.static
be configured so that clients can request files in theassets
directory?A call to
app.use(express.static('assets'))
should be added to the server script. -
How can the response’s type, which is specified in the request’s header, be identified within an Express route?
The client sets acceptable types in the
accept
header field, which is mapped to thereq.headers.accept
property. -
Which method of the
res
(response) route parameter generates a response in JSON format from a JavaScript array calledcontent
?The
res.json()
method:res.json(content)
.
Answers to Explorational Exercises
-
By default, Express template files are in the
views
directory. How can this setting be modified so that template files are stored intemplates
?The directory can be defined in the initial script settings with
app.set('views', './templates')
. -
Suppose a client receives an HTML response with no title (i.e.
<title></title>
). After verifying the EJS template, the developer finds the<title><% title %></title>
tag in thehead
section of the file. What is the likely cause of the problem?The
<%= %>
tag should be used to enclose the contents of a variable, as in<%= title %>
. -
Use EJS template tags to write a
<h2></h2>
HTML tag with the contents of the JavaScript variableh2
. This tag should be rendered only if theh2
variable is not empty.<% if ( h2 != "" ) { %> <h2><%= h2 %></h2> <% } %>