Skip to content

Latest commit

 

History

History
313 lines (213 loc) · 14.6 KB

File metadata and controls

313 lines (213 loc) · 14.6 KB

Express

📖 Deeper dive reading: MDN Express/Node introduction

In the previous instruction you saw how to use Node.js to create a simple web server. This works great for little projects where you are trying to quickly serve up some web content, but to build a production-ready application you need a framework with a bit more functionality for easily implementing a full web service. This is where the Node package Express come in. Express provides support for:

  1. Routing requests for service endpoints
  2. Manipulating HTTP requests with JSON body content
  3. Generating HTTP responses
  4. Using middleware to add functionality

Express was created by TJ Holowaychuk and is currently maintained by the Open.js Foundation.

TJ Holowaychuk

“People tell you to not reinvent things, but I think you should … it will teach you things”

— TJ Holowaychuk

Everything in Express revolves around creating and using HTTP routing and middleware functions. You create an Express application by using NPM to install the Express package and then calling the express constructor to create the Express application and listen for HTTP requests on a desired port.

➜ npm install express
const express = require('express');
const app = express();

app.listen(8080);

With the app object you can now add HTTP routing and middleware functions to the application.

Defining routes

HTTP endpoints are implemented in Express by defining routes that call a function based upon an HTTP path. The Express app object supports all of the HTTP verbs as functions on the object. For example, if you want to have a route function that handles an HTTP GET request for the URL path /store/provo you would call the get method on the app.

app.get('/store/provo', (req, res, next) => {
  res.send({ name: 'provo' });
});

The get function takes two parameters, a URL path matching pattern, and a callback function that is invoked when the pattern matches. The path matching parameter is used to match against the URL path of an incoming HTTP request.

The callback function has three parameters that represent the HTTP request object (req), the HTTP response object (res), and the next routing function that Express expects to be called if this routing function wants another function to generate a response.

The Express app compares the routing function patterns in the order that they are added to the Express app object. So if you have two routing functions with patterns that both match, the first one that was added will be called and given the next matching function in the next parameter.

In our example above we hard coded the store name to be provo. A real store endpoint would allow any store name to be provided as a parameter in the path. Express supports path parameters by prefixing the parameter name with a colon (:). Express creates a map of path parameters and populates it with the matching values found in the URL path. You then reference the parameters using the req.params object. Using this pattern you can rewrite our getStore endpoint as follows.

app.get('/store/:storeName', (req, res, next) => {
  res.send({ name: req.params.storeName });
});

If we run our JavaScript using node we can see the result when we make an HTTP request using curl.

➜ curl localhost:8080/store/orem
{"name":"orem"}

If you wanted an endpoint that used the POST or DELETE HTTP verb then you just use the post or delete function on the Express app object.

The route path can also include a limited wildcard syntax or even full regular expressions in path pattern. Here are a couple route functions using different pattern syntax.

// Wildcard - matches /store/x and /star/y
app.put('/st*suffix/:storeName', (req, res) => res.send({ update: req.params.storeName, prefix: req.params.suffix }));

// Pure regular expression
app.delete(/\/store\/(.+)/, (req, res) => res.send({ delete: req.params[0] }));

Notice that in these examples the next parameter was omitted. Since we are not calling next we do not need to include it as a parameter. However, if you do not call next then no following middleware functions will be invoked for the request.

Using middleware

📖 Deeper dive reading: Express Middleware

The standard Mediator/Middleware design pattern has two pieces: a mediator and middleware. Middleware represents componentized pieces of functionality. The mediator loads the middleware components and determines their order of execution. When a request comes to the mediator it then passes the request around to the middleware components. Following this pattern, Express is the mediator, and middleware functions are the middleware components.

Express comes with a standard set of middleware functions. These provide functionality like routing, authentication, CORS, sessions, serving static web files, cookies, and logging. Some middleware functions are provided by default, and other ones must be installed using NPM before you can use them. You can also write your own middleware functions and use them with Express.

A middleware function looks very similar to a routing function. That is because routing functions are also middleware functions. The only difference is that routing functions are only called if the associated pattern matches. Middleware functions are always called for every HTTP request unless a preceding middleware function does not call next. A middleware function has the following pattern:

function middlewareName(req, res, next)

The middleware function parameters represent the HTTP request object (req), the HTTP response object (res), and the next middleware function to pass processing to. You should usually call the next function after completing processing so that the next middleware function can execute.

Middleware

Creating your own middleware

As an example of writing your own middleware, you can create a function that logs out the URL of the request and then passes on processing to the next middleware function.

app.use((req, res, next) => {
  console.log(req.originalUrl);
  next();
});

Remember that the order that you add your middleware to the Express app object controls the order that the middleware functions are called. Any middleware that does not call the next function after doing its processing, stops the middleware chain from continuing.

Builtin middleware

In addition to creating your own middleware functions, you can use a built-in middleware function. Here is an example of using the static middleware function. This middleware responds with static files, found in a given directory, that match the request URL.

app.use(express.static('public'));

Now if you create a subdirectory in your project directory and name it public you can serve up any static content that you would like. For example, you could create an index.html file that is the default content for your web service. Then when you call your web service without any path the index.html file will be returned.

Third party middleware

You can also use third party middleware functions by using NPM to install the package and including the package in your JavaScript with the require function. The following uses the cookie-parser package to simplify the generation and access of cookies.

➜ npm install cookie-parser
const cookieParser = require('cookie-parser');

app.use(cookieParser());

app.post('/cookie/:name/:value', (req, res) => {
  res.cookie(req.params.name, req.params.value);
  res.send({ cookie: `${req.params.name}:${req.params.value}` });
});

app.get('/cookie', (req, res) => {
  res.send({ cookie: req.cookies });
});

It is common for middleware functions to add fields and functions to the req and res objects so that other middleware can access the functionality they provide. You see this happening when the cookie-parser middleware adds the req.cookies object for reading cookies, and also adds the res.cookie function in order to make it easy to add a cookie to a response.

You can use Curl to experiment with the above cookie code with the following commands.

➜ curl -c cookies.txt -X POST localhost:3000/cookie/type/chocolate
{"cookie":"type:chocolate"}

➜ curl -b cookies.txt localhost:3000/cookie
{"cookie":{"type":"chocolate"}}

Error handling middleware

You can also add middleware for handling errors that occur. Error middleware looks similar to other middleware functions, but it takes an additional err parameter that contains the error.

function errorMiddlewareName(err, req, res, next)

If you wanted to add a simple error handler for anything that might go wrong while processing HTTP requests you could add the following.

app.use(function (err, req, res, next) {
  res.status(500).send({ type: err.name, message: err.message });
});

We can test that our error middleware is getting used by adding a new endpoint that generates an error.

app.get('/error', (req, res, next) => {
  throw new Error('Trouble in river city');
});

Now if we use curl to call our error endpoint we can see that the response comes from the error middleware.

➜ curl localhost:8080/error
{"type":"Error","message":"Trouble in river city"}

Putting it all together

Here is a full example of our web service built using Express.

const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();

// Third party middleware - Cookies
app.use(cookieParser());

app.post('/cookie/:name/:value', (req, res) => {
  res.cookie(req.params.name, req.params.value);
  res.send({ cookie: `${req.params.name}:${req.params.value}` });
});

app.get('/cookie', (req, res) => {
  res.send({ cookie: req.cookies });
});

// Creating your own middleware - logging
app.use((req, res, next) => {
  console.log(req.originalUrl);
  next();
});

// Built in middleware - Static file hosting
app.use(express.static('public'));

// Routing middleware

// Get store endpoint
app.get('/store/:storeName', (req, res) => {
  res.send({ name: req.params.storeName });
});

// Update store endpoint
app.put('/st*suffix/:storeName', (req, res) => res.send({ update: req.params.storeName, prefix: req.params.suffix }));

// Delete store endpoint
app.delete(/\/store\/(.+)/, (req, res) => res.send({ delete: req.params[0] }));

// Error middleware
app.get('/error', (req, res, next) => {
  throw new Error('Trouble in river city');
});

app.use(function (err, req, res, next) {
  res.status(500).send({ type: err.name, message: err.message });
});

// Listening to a network port
const port = 8080;
app.listen(port, function () {
  console.log(`Listening on port ${port}`);
});

Debugging an Express web service

Let's take a moment to talk about how you can debug a web service running with the Express package under Node.js. Using the code that you created above, set a breakpoint on the code inside the getStore endpoint callback and another breakpoint on the app.listen call. Start debugging by pressing F5. The debugger should stop on the listen call where you can inspect the app variable. Press F5 again to continue running. Now open up your browser and set the location to localhost:8080/store/provo. This should hit the breakpoint on the endpoint. Take some time to inspect the req object. You should be able to see what the HTTP method is, what parameters are provided, and what the path currently is. Press F5 to continue. Your browser should display the JSON object that you returned from your endpoint.

Make another request from our browser, but this time include some query parameters. Something like http://localhost:8080/store/orem?order=2. Requesting that URL should cause your breakpoint to hit again where you can see the URL changes reflected in the req object.

Now, instead of pressing F5 to continue, press F11 to step into the res.send function. This will take you out of your code and into the Express code that handles sending a response. Because you installed the Express package using NPM, all of Express's source code is sitting in the node_modules directory. You can also set breakpoints there, examine variables, and step into functions. Debugging into popular packages is a great way to learn how to code by seeing how really good programmers do things. Take some time to walk around Holowaychuk's code and see if you can understand what it is doing.

Debug step in

☑ Assignment

Create a web service with Express using the following steps.

  1. Open your console.

  2. Create a directory named testExpress, and change into that directory

    mkdir testExpress
    cd testExpress
  3. Initialize the directory for use with NPM.

    npm init -y
  4. Install the express and cookie-parser packages.

    npm install express cookie-parser
  5. Create a file named index.js and paste the example code given above.

  6. Create a directory named public and add an index.html file with some basic html to the directory.

    mkdir public
    print '<h1>Hello express</h1>' > public/index.html
  7. Run your web service using node (node index.js)

    node index.js
  8. Open another console window and use Curl to try out your web service by making requests to the endpoints.

    curl localhost:8080
    curl localhost:8080/error
    curl localhost:8080/store/orem
    curl -X PUT localhost:8080/st/orem
    curl -X DELETE localhost:8080/store/orem
    curl -X POST -c cookies.txt localhost:8080/cookie/express/tj
    curl -b cookies.txt localhost:8080/cookie
  9. Develop a mental model in your head about what these commands are doing and how your service is responding. Perhaps creating a sequence diagram will help clarify the interaction if it is still unclear.

    HTTP request

  10. Debug your application by setting breakpoints, inspecting variables, and walking through the code.

If your section of this course requires that you submit assignments for grading: Submit the output from the curl commands to the Canvas assignment.