Linux Professional Institute Learning Logo.
Skip to main content
  • Home
    • All Resources
    • LPI Learning Materials
    • Become a Contributor
    • Publishing Partners
    • Become a Publishing Partner
    • About
    • FAQ
    • Contributors
    • Roadmap
    • Contact
  • LPI.org
035.2 Lesson 2
Topic 031: Software Development and Web Technologies
031.1 Software Development Basic
  • 031.1 Lesson 1
031.2 Web Application Architecture
  • 031.2 Lesson 1
031.3 HTTP Basics
  • 031.3 Lesson 1
Topic 032: HTML Document Markup
032.1 HTML Document Anatomy
  • 032.1 Lesson 1
032.2 HTML Semantics and Document Hierarchy
  • 032.2 Lesson 1
032.3 HTML References and Embedded Resources
  • 032.3 Lesson 1
032.4 HTML Forms
  • 032.4 Lesson 1
Topic 033: CSS Content Styling
033.1 CSS Basics
  • 033.1 Lesson 1
033.2 CSS Selectors and Style Application
  • 033.2 Lesson 1
033.3 CSS Styling
  • 033.3 Lesson 1
033.4 CSS Box Model and Layout
  • 033.4 Lesson 1
Topic 034: JavaScript Programming
034.1 JavaScript Execution and Syntax
  • 034.1 Lesson 1
034.2 JavaScript Data Structures
  • 034.2 Lesson 1
034.3 JavaScript Control Structures and Functions
  • 034.3 Lesson 1
  • 034.3 Lesson 2
034.4 JavaScript Manipulation of Website Content and Styling
  • 034.4 Lesson 1
Topic 035: NodeJS Server Programming
035.1 NodeJS Basics
  • 035.1 Lesson 1
035.2 NodeJS Express Basics
  • 035.2 Lesson 1
  • 035.2 Lesson 2
035.3 SQL Basics
  • 035.3 Lesson 1
How to get certified
  1. Topic 035: NodeJS Server Programming
  2. 035.2 NodeJS Express Basics
  3. 035.2 Lesson 2

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 send 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

  1. How should express.static be configured so that clients can request files in the assets directory?

  2. How can the response’s type, which is specified in the request’s header, be identified within an Express route?

  3. Which method of the res (response) route parameter generates a response in JSON format from a JavaScript array called content?

Explorational Exercises

  1. By default, Express template files are in the views directory. How can this setting be modified so that template files are stored in templates?

  2. 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 the head section of the file. What is the likely cause of the problem?

  3. Use EJS template tags to write a <h2></h2> HTML tag with the contents of the JavaScript variable h2. This tag should be rendered only if the h2 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

  1. How should express.static be configured so that clients can request files in the assets directory?

    A call to app.use(express.static('assets')) should be added to the server script.

  2. 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 the req.headers.accept property.

  3. Which method of the res (response) route parameter generates a response in JSON format from a JavaScript array called content?

    The res.json() method: res.json(content).

Answers to Explorational Exercises

  1. By default, Express template files are in the views directory. How can this setting be modified so that template files are stored in templates?

    The directory can be defined in the initial script settings with app.set('views', './templates').

  2. 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 the head 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 %>.

  3. Use EJS template tags to write a <h2></h2> HTML tag with the contents of the JavaScript variable h2. This tag should be rendered only if the h2 variable is not empty.

    <% if ( h2 != "" ) { %>
    <h2><%= h2 %></h2>
    <% } %>

Linux Professional Insitute Inc. All rights reserved. Visit the Learning Materials website: https://learning.lpi.org
This work is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

Next Lesson

035.3 SQL Basics (035.3 Lesson 1)

Read next lesson

Linux Professional Insitute Inc. All rights reserved. Visit the Learning Materials website: https://learning.lpi.org
This work is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

LPI is a non-profit organization.

© 2023 Linux Professional Institute (LPI) is the global certification standard and career support organization for open source professionals. With more than 200,000 certification holders, it's the world’s first and largest vendor-neutral Linux and open source certification body. LPI has certified professionals in over 180 countries, delivers exams in multiple languages, and has hundreds of training partners.

Our purpose is to enable economic and creative opportunities for everybody by making open source knowledge and skills certification universally accessible.

  • LinkedIn
  • flogo-RGB-HEX-Blk-58 Facebook
  • Twitter
  • Contact Us
  • Privacy and Cookie Policy

Spot a mistake or want to help improve this page? Please let us know.

© 1999–2023 The Linux Professional Institute Inc. All rights reserved.