035.2 Lesson 1
Certificate: |
Web Development Essentials |
---|---|
Version: |
1.0 |
Topic: |
035 NodeJS Server Programming |
Objective: |
035.2 NodeJS Express Basics |
Lesson: |
1 of 2 |
Introduction
Express.js, or simply Express, is a popular framework that runs on Node.js and is used to write HTTP servers that handle requests from web application clients. Express supports many ways to read parameters sent over HTTP.
Initial Server Script
To demonstrate Express’s basic features for receiving and handling requests, let’s simulate an application that requests some information from the server. In particular, the example server:
-
Provides an
echo
function, which simply returns the message sent by the client. -
Tells the client its IP address upon request.
-
Uses cookies to identify known clients.
The first step is to create the JavaScript file that will operate as the server. Using npm
, create a directory called myserver
with the JavaScript file:
$ mkdir myserver $ cd myserver/ $ npm init
For the entry point, any filename can be used. Here we will use the default filename: index.js
. The following listing shows a basic index.js
file that will be used as the entry point for our server:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.get('/', (req, res) => {
res.send('Request received')
})
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
Some important constants for the server configuration are defined in the first lines of the script. The first two, express
and app
, correspond to the included express
module and an instance of this module that runs our application. We will add the actions to be performed by the server to the app
object.
The other two constants, host
and port
, define the host and communication port associated to the server.
If you have a publicly accessible host, use its name instead of myserver
as the value of host
. If you don’t provide the host name, Express will default to localhost
, the computer where the application runs. In that case, no outside clients will be able to reach the program, which may be fine for testing but offers little value in production.
The port needs to be provided, or the server will not start.
This script attaches only two procedures to the app
object: the app.get()
action that answers requests made by clients through HTTP GET
, and the app.listen()
call, which is required to activate the server and assigns it a host and port.
To start the server, just run the node
command, providing the script name as an argument:
$ node index.js
As soon as the message Server ready at http://myserver:8080
appears, the server is ready to receive requests from an HTTP client. Requests can be made from a browser on the same computer where the server is running, or from another machine that can access the server.
All transaction details we’ll see here are shown in the browser if you open a window for the developer console. Alternatively, the curl
command can be used for HTTP communication and allows you to inspect connection details more easily. If you are not familiar with the shell command line, you can create an HTML form to submit requests to a server.
The following example shows how to use the curl
command on the command line to make an HTTP request to the newly deployed server:
$ curl http://myserver:8080 -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/html; charset=utf-8 < Content-Length: 16 < ETag: W/"10-1WVvDtVyAF0vX9evlsFlfiJTT5c" < Date: Fri, 02 Jul 2021 14:35:11 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Request received
The -v
option of the curl
command displays all the request and response headers, as well as other debugging information. The lines starting with >
indicate the request headers sent by the client and the lines starting with <
indicate the response headers sent by the server. Lines starting with *
are information generated by curl
itself. The content of the response is displayed only at the end, which in this case is the line Request received
.
The service’s URL, which in this case contains the server’s hostname and port (http://myserver:8080
), were given as arguments to the curl
command. Because no directory or filename is given, these default to the root directory /
. The slash turns up as the request file in the > GET / HTTP/1.1
line, which is followed in the output by the hostname and port.
In addition to displaying HTTP connection headers, the curl
command assists application development by allowing you to send data to the server using different HTTP methods and in different formats. This flexibility makes it easier to debug any problems and implement new features on the server.
Routes
The requests the client can make to the server depend on what routes have been defined in the index.js
file. A route specifies an HTTP method and defines a path (more precisely, a URI) that can be requested by the client.
So far, the server has only one route configured:
app.get('/', (req, res) => {
res.send('Request received')
})
Even though it is a very simple route, simply returning a plain text message to the client, it is enough to identify the most important components that are used to structure most routes:
-
The HTTP method served by the route. In the example, the HTTP
GET
method is indicated by theget
property of theapp
object. -
The path served by the route. When the client does not specify a path for the request, the server uses the root directory, which is the base directory set aside for use by the web server. A later example in this chapter uses the path
/echo
, which corresponds to a request made tomyserver:8080/echo
. -
The function executed when the server receives a request on this route, usually written in abbreviated form as an arrow function because the syntax
=>
points to the definition of the nameless function. Thereq
parameter (short for “request”) andres
parameter (short for “response”) give details about the connection, passed to the function by the app instance itself.
POST Method
To extend the functionality of our test server, let’s see how to define a route for the HTTP POST
method. It’s used by clients when they need to send extra data to the server beyond those included in the request header. The --data
option of the curl
command automatically invokes the POST
method, and includes content that will be sent to the server via POST
. The POST / HTTP/1.1
line in the following output shows that the POST
method was used. However, our server defined only a GET
method, so an error occurs when we use curl
to send a request via POST
:
$ curl http://myserver:8080/echo --data message="This is the POST request body" * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > POST / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > Content-Length: 37 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 37 out of 37 bytes * Mark bundle as not supporting multiuse < HTTP/1.1 404 Not Found < X-Powered-By: Express < Content-Security-Policy: default-src 'none' < X-Content-Type-Options: nosniff < Content-Type: text/html; charset=utf-8 < Content-Length: 140 < Date: Sat, 03 Jul 2021 02:22:45 GMT < Connection: keep-alive < <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Cannot POST /</pre> </body> </html> * Connection #0 to host myserver left intact
In the previous example, running curl
with the parameter --data message="This is the POST request body"
is equivalent to submitting a form containing the text field named message
, filled with This is the POST request body
.
Because the server is configured with only one route for the /
path, and that route only responds to the HTTP GET
method, so the response header has the line HTTP/1.1 404 Not Found
. In addition, Express automatically generated a short HTML response with the warning Cannot POST
.
Having seen how to generate a POST
request through curl
, let’s write an Express program that can successfully handle the request.
First, note that the Content-Type
field in the request header says that the data sent by the client is in the application/x-www-form-urlencoded
format. Express does not recognize that format by default, so we need to use the express.urlencoded
module. When we include this module, the req
object—passed as a parameter to the handler function—has the req.body.message
property set, which corresponds to the message
field sent by the client. The module is loaded with app.use
, which should be placed before the declaration of routes:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.use(express.urlencoded({ extended: true }))
Once this is done, it would be enough to change app.get
to app.post
in the existing route to fulfill requests made via POST
and to recover the request body:
app.post('/', (req, res) => {
res.send(req.body.message)
})
Instead of replacing the route, another possibility would be to simply add this new route, because Express identifies the HTTP method in the request header and uses the appropriate route. Because we are interested in adding more than one functionality to this server, it is convenient to separate each one with its own path, such as /echo
and /ip
.
Path and Function Handler
Having defined which HTTP method will respond to the request, we now need to define a specific path for the resource and a function that processes and generates a response to the client.
To expand the echo
functionality of the server, we can define a route using the POST
method with the path /echo
:
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
The req
parameter of the handler function contains all the request details stored as properties. The content of the message
field in the request body is available in the req.body.message
property. The example simply sends this field back to the client through the res.send(req.body.message)
call.
Remember that the changes you make take effect only after the server is restarted. Because you are running the server from a terminal window during the examples in this chapter, you can shut down the server by pressing kbd:[Ctrl+C] on that terminal. Then rerun the server through the node index.js
command. The response obtained by the client to the curl
request we showed before is now successful:
$ curl http://myserver:8080/echo --data message="This is the POST request body" This is the POST request body
Other Ways to Pass and Return Information in a GET Request
It might be excessive to use the HTTP POST
method if only short text messages like the one used in the example will be sent. In such cases, data can be sent in a query string that starts with a question mark. Thus, the string ?message=This+is+the+message
could be included within the request path of the HTTP GET
method. The fields used in the query string are available to the server in the req.query
property. Therefore, a field named message
is available in the req.query.message
property.
Another way to send data via the HTTP GET
method is to use Express’s route parameters:
app.get('/echo/:message', (req, res) => {
res.send(req.params.message)
})
The route in this example matches requests made with the GET
method using the path /echo/:message
, where :message
is a placeholder for any term sent with that label by the client. These parameters are accessible in the req.params
property. With this new route, the server’s echo
function can be requested more succinctly by the client:
$ curl http://myserver:8080/echo/hello hello
In other situations, the information the server needs to process the request do not need to be explicitly provided by the client. For instance, the server has another way to retrieve the client’s public IP address. That information is present in the req
object by default, in the req.ip
property:
app.get('/ip', (req, res) => {
res.send(req.ip)
})
Now the client can request the /ip
path with the GET
method to find its own public IP address:
$ curl http://myserver:8080/ip 187.34.178.12
Other properties of the req
object can be modified by the client, especially the request headers available in req.headers
. The req.headers.user-agent
property, for example, identifies which program is making the request. Although it is not common practice, the client can change the contents of this field, so the server should not use it to reliably identify a particular client. It is even more important to validate the data explicitly provided by the client, to avoid inconsistencies in boundaries and formats that could adversely affect the application.
Adjustments to the Response
As we’ve seen in previous examples, the res
parameter is responsible for returning a response to the client. Furthermore, the res
object can change other aspects of the response. You may have noticed that, although the responses we’ve implemented so far are just brief plain text messages, the Content-Type
header of the responses is using text/html; charset=utf-8
. Although this does not prevent the plain text response from being accepted, it will be more correct if we redefine this field in the response header to text/plain
with the setting res.type('text/plain')
.
Other types of response adjustments involve using cookies, which allow the server to identify a client that has previously made a request. Cookies are important for advanced features, such as creating private sessions that associate requests to a specific user, but here we’ll just look at a simple example of how to use a cookie to identify a client that has previously accessed the server.
Given the modularized design of Express, cookie management must be installed with the npm
command before being used in the script:
$ npm install cookie-parser
After installation, cookie management must be included in the server script. The following definition should be included near the beginning of the file:
const cookieParser = require('cookie-parser')
app.use(cookieParser())
To illustrate the use of cookies, let’s modify the route’s handler function with the /
root path that already exists in the script. The req
object has a req.cookies
property, where cookies sent in the request header are kept. The res
object, on the other hand, has a res.cookie()
method that creates a new cookie to be sent to the client. The handler function in the following example checks whether a cookie with the name known
exists in the request. If such a cookie does not exist, the server assumes that this is a first-time visitor and sends it a cookie with that name through the res.cookie('known', '1')
call. We arbitrarily assign the value 1
to the cookie because it is supposed to have some content, but the server doesn’t consult that value. This application just assumes that the simple presence of the cookie indicates that the client has already requested this route before:
app.get('/', (req, res) => {
res.type('text/plain')
if ( req.cookies.known === undefined ){
res.cookie('known', '1')
res.send('Welcome, new visitor!')
}
else
res.send('Welcome back, visitor');
})
By default, curl
does not use cookies in transactions. But it has options to store (-c cookies.txt
) and send stored cookies (-b cookies.txt
):
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/plain; charset=utf-8 * Added cookie known="1" for domain myserver, path /, expire 0 < Set-Cookie: known=1; Path=/ < Content-Length: 21 < ETag: W/"15-l7qrxcqicl4xv6EfA5fZFWCFrgY" < Date: Sat, 03 Jul 2021 23:45:03 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Welcome, new visitor!
Because this command was the first access since cookies were implemented on the server, the client did not have any cookies to include in the request. As expected, the server did not identify the cookie in the request and therefore included the cookie in the response headers, as indicated in the Set-Cookie: known=1; Path=/
line of the output. Since we have enabled cookies in curl
, a new request will include the cookie known=1
in the request headers, allowing the server to identify the cookie’s presence:
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > Cookie: known=1 > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/plain; charset=utf-8 < Content-Length: 21 < ETag: W/"15-ATq2flQYtLMYIUpJwwpb5SjV9Ww" < Date: Sat, 03 Jul 2021 23:45:47 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Welcome back, visitor
Cookie Security
The developer should be aware of potential vulnerabilities when using cookies to identify clients making requests. Attackers can use techniques such as cross-site scripting (XSS) and cross-site request forgery (CSRF) to steal cookies from a client and thereby impersonate them when making a request to the server. Generally speaking, these types of attacks use non-validated comment fields or meticulously constructed URLs to insert malicious JavaScript code into the page. When executed by an authentic client, this code can copy valid cookies and store them or forward them to another destination.
Therefore, especially in professional applications, it is important to install and use more specialized Express features, known as middleware. The express-session
or cookie-session
module provide more complete and secure control over session and cookie management. These components enable extra controls to prevent cookies from being diverted from their original issuer.
Guided Exercises
-
How can the content of the
comment
field, sent within a query string of the HTTPGET
method, be read in a handler function? -
Write a route that uses the HTTP
GET
method and the/agent
path to send back to the client the contents of theuser-agent
header. -
Express.js has a feature called route parameters, where a path such as
/user/:name
can be used to receive thename
parameter sent by the client. How can thename
parameter be accessed within the handler function of the route?
Explorational Exercises
-
If a server’s host name is
myserver
, which Express route would receive the submission in the form below?<form action="/contact/feedback" method="post"> ... </form>
-
During server development, the programmer is not able to read the
req.body
property, even after verifying that the client is correctly sending the content via the HTTPPOST
method. What is a likely cause for this problem? -
What happens when the server has a route set to the path
/user/:name
and the client makes a request to/user/
?
Summary
This lesson explains how to write Express scripts to receive and handle HTTP requests. Express uses the concept of routes to define the resources available to clients, which gives you great flexibility to build servers for any kind of web application. This lesson goes through the following concepts and procedures:
-
Routes that use the HTTP
GET
and HTTPPOST
methods. -
How form data is stored in the
request
object. -
How to use route parameters.
-
Customizing response headers.
-
Basic cookie management.
Answers to Guided Exercises
-
How can the content of the
comment
field, sent within a query string of the HTTPGET
method, be read in a handler function?The
comment
field is available in thereq.query.comment
property. -
Write a route that uses the HTTP
GET
method and the/agent
path to send back to the client the contents of theuser-agent
header.app.get('/agent', (req, res) => { res.send(req.headers.user-agent) })
-
Express.js has a feature called route parameters, where a path such as
/user/:name
can be used to receive thename
parameter sent by the client. How can thename
parameter be accessed within the handler function of the route?The
name
parameter is accessible in thereq.params.name
property.
Answers to Explorational Exercises
-
If a server’s host name is
myserver
, which Express route would receive the submission in the form below?<form action="/contact/feedback" method="post"> ... </form>
app.post('/contact/feedback', (req, res) => { ... })
-
During server development, the programmer is not able to read the
req.body
property, even after verifying that the client is correctly sending the content via the HTTPPOST
method. What is a likely cause for this problem?The programmer did not include the
express.urlencoded
module, which lets Express extract the body of a request. -
What happens when the server has a route set to the path
/user/:name
and the client makes a request to/user/
?The server will issue a
404 Not Found
response, because the route requires the:name
parameter to be provided by the client.