5-5 Web app server

Express.js (official website) is a web framework to simplify development of Web applications. It supports:

  • Routing – different request processing based on URL patterns
  • Middleware – chainable processing of requests and responses
  • Template – generate HTML output from variables

This lab covers how to define routes to capture HTTP requests for a web app / service endpoints. We’ll discuss how to generate simple HTTP responses. Finally, we’ll cover the usage of common middlewares.

Handling requests at routes

This example shows the basic structure of an Express app.

import express from 'express';
// create an Express application, which will handle HTTP requests
var app = express();

// a route in the app, which handles GET request for the path '/'
app.get('/', (req, res) => {
  res.send('hello world');
});

// more routes ...

// start listening at TCP port 3000
app.listen(3000);

When a web app receives an HTTP request, it has to decide how to handle the request, and return the result as an HTTP response. Such decision usually depends on the HTTP method (e.g. GET, POST, PUT) and the path in the URL (e.g. “/about.html”). The combination of HTTP method and the URL path is usually referred to as an endpoint of the web app or web API.

In an Express app, we define a route to describe how to process HTTP requests at an endpoint. The general syntax is app.HTTP_method(path, callback), where HTTP_method stands for GET, POST, PUT, PATCH, DELETE, etc. The callback is sometimes called route handler, and can take 2 or more parameters. Often, we only use the first two parameters called req and res: req refers to the incoming HTTP request, and res is an object that is used to build the HTTP response. The following is a route for GET request at URL ‘/about.html’. The route returns a text response.

// app is an Express app

app.get('/about.html', (req, res) => {
  // When the Express app receives a GET request
  // for the path '/about.html', it returns a text response
  res.send('This is a simple example of route')
});

Some web apps include parameters in the URL path. You can easily extract these parameters in Express routes. Consider the sample code below, which shows a web app to check lecture hours. When the app receives a GET request for the path ‘/lecture/comp312’, it returns a response of its lecture hours. The parameters are available in the object req.params.

Check the source code of app1.mjs for more detail.

// retrieve the lecture time
app.get('/lecture/:code', (req, res) => {
  // in a real app, we'd query a database ...
  if (req.params.code=='comp312') {
    res.send('Tue, Thu: 10:00-11:30am')
  } else if (req.params.code=='comp311') {
    // ...
  }
})

The above only describes the basics of routing in Express. Refer to the online guide for more possibilities.

Making responses

You can use the res object in the route callback to build a response. (online reference) The res object has several common methods:

  • to set status code and headers

    • res.status(code) sets the HTTP response code
    • res.type(type) sets the Content-Type of the response
    • res.append(field, [value]) appends a header with the given values
    • res.cookie(name, value) sets a cookie
  • to send content in response body

    • res.send(body) sends the response body. If body is a string, text/html is assumed. If body is a JavaScript object, the method converts it to a JSON string (JSON.stringify()) and use the Content type application/json.
    • res.json(body) returns the data as JSON payload
    • res.sendFile(path) returns a file as response. The method determines a suitable MIME type based on file type.
    • res.redirect([status], path) redirects to the given URL
    • res.end() usually used to send the response with an empty body

Some of these methods can be chained.

// retrieve the lecture time
app.get('/no-such-resource', (req, res) => {
  res.status(404).end()
})

Check the source code of app2.mjs for demonstration of how to build responses with these methods.

Order of routes

The order of routes is significant! An Express app checks the routes in the order they are defined. When a route matches the HTTP methods and URL path, the Express app executes its callback function to handle the request. The callback usually returns an HTTP response with res.send() or similar methods. When a response is returned, the Express app will stop checking the remaining routes. Once a response body is sent, it is said to have stopped the request-response cycle in Express. Express will not search the remaining routes (or middlewares) for the current request, and the processing of the request is completed.

app.get('/lecture/comp312', (req, res) => {
  // special response for comp312
  res.send('...');
});

// retrieve the lecture time
app.get('/lecture/:code', (req, res) => {
  // search database, return a response
  res.send('...');
});

app.get('/lecture/comp113', (req, res) => {
  // special response for comp113. But sorry, this route never runs!
  res.send('...');
});

Handling URL-encoded payload

Express decodes the query string of the request URL and deserializes it into a JavaScript object at req.query.

// handle requests like GET /add?a=3&b=5
app.get('/add', (req, res) => {
  let a = parseFloat(req.query.a);
  let b = parseFloat(req.query.b);
  res.json({
    method: 'GET',
    a: a, b: b, sum: a+b  
  });
});

To handle URL-encoded payload in request body, e.g. in POST requests, you need to let a middleware to do the decoding before the route to process the data. The middleware express.urlencoded deserializes the URL-encoded payload into a JavaScript object at req.body. (Similarly, there is a built-in middleware express.json to deserialize JSON payload.)

app.use(express.urlencoded({extended: true}));

app.post('/add', (req, res) => {
  let a = parseFloat(req.body.a);
  let b = parseFloat(req.body.b);
  res.json({
    method: 'POST',
    a: a, b: b, sum: a+b  
  });
});

Check the source code of app3.mjs for demonstration of how to build responses with these methods.

Middlewares

In Express.js, both middlewares and route handlers are callback functions. When you set up an Express app, you register middlewares with app.use() and route handlers with app.get() (and also, for other HTTP method, e.g. app.post()) in sequential order to build a middleware stack.

Refer to the figures in the article Express Middlewares, Demystified and Understanding Express Middleware.

When an Express app server receives an HTTP request, it will pass the request to each callback in the middleware stack in order. When a callback calls next(), control will be transferred to the next in the stack. On the other hand, if a callback returns an HTTP response to the client (e.g. by calling res.json()), it will stop the request-response cycle.

During the request-response cycle, a middleware function can make changes to the req or res object, send a response body to complete the processing of the current request, or pass the control to the next callback in the middleware stack. For more information, please refer to the official guide on writing middlewares and using middlewares.

app.use( (req, res, next) => {
  // after logging this request, pass to next
  console.log(`${req.method} - ${req.baseUrl}`);
  next();
});

app.use( (req, res, next) => {
  // has this user logged in?
  if (!sessionActive) {
    res.redirect('/login.html'); return;
  }
  next();
});

// other routes and middlewares

A useful built-in middleware is express.static. This searches a given directory to find files that match the path in the URL being request. If found, the middleware returns the file as a response. Otherwise, it pass control to the next callback in the middleware stack. express.static is effectively a mini web server.

// most routes and middlewares should be placed before the static middleware

app.use(express.static(__dirname + '/public'));

// if no file matches the request URL, the following routes will be checked.

app.get('special_page.html', (req,res) => { });

If a file with the same name as a route (e.g. special_page.html) is added to the /public folder, the express.static middleware will stop the request-response cycle, and that route will no longer be used. Therefore, we usually put the static middleware near the bottom of the middleware stack.

Check the source code of app4.mjs for demonstration of how to build responses with these methods.

Downloadable sample code