JSON is used as the preferred encoding for the data, both in request and response bodies.
Some REST specifications allow the caller to specify the format (JSON, XML, CSV, etc.), but we will be supporting only JSON, because we don’t expect your API to be exposed to other consumers who may want this choice. So we will skip the need for the client to specify the format.
Chapter 5 ■ express rest apIs
72
Express
I briefly touched upon Express and how to serve static files using Express in the Hello World chapter. But Express can do much more than just serve static files. Express is a minimal, yet, flexible web application framework. It’s minimal in the sense that by itself, Express does very little. It relies on other modules (middleware) to provide the functionality that most applications will need. The concepts are simple and easy to understand.
Routing
The first concept is that of routing. At the heart of Express is a router, which essentially takes a client request, matches it against any routes that are present, and executes the handler function that is associated with that route. The handler function is expected to generate the appropriate response.
A route specification consists of an HTTP method (GET, POST, etc.), a path
specification that matches the request URI, and the route handler. The handler is passed in a request object and a response object. The request object can be inspected to get the various details of the request, and the response object’s methods can be used to send the response to the client. All this may seem a little overwhelming, so let’s just start with a simple example and explore the details:
const app = express();
app.get('/hello', (req, res) => { res.send('Hello World');
});
To use Express, you first need to create an application using its root-level exported function. You do that using const app = express();. You can now set up routes using this app. To set up a route, you use a function to indicate which HTTP method; for example, to handle the GET HTTP method, you use the app’s get() function. To this function, you pass the pattern to match and a function to deal with the request if it does match.
Request Matching
When a request is received, the first thing that happens is request matching. The request’s method is matched with the route method (the get function was called on app, indicating it should match only GET HTTP methods), and also the request URL with the path spec ('/hello' in the above example). If a request matches this spec, then the handler is called. In other words, the above example reads “If you receive a GET request to the URL / hello, then execute this piece of code.”
The method as well as the path need not be very specific. Normally, you would use app.get(), app.post(), app.put(), etc., but if you want to say “any method,” you could use app.all(). The path specification can also take regular expression-like patterns (like '/*.
do') or regular expressions themselves. But regular expressions in paths are rarely used.
Route parameters in the path are used often, so I’ll discuss them in a little more detail.
Chapter 5 ■ express rest apIs
Route Parameters
Route parameters are named segments in the path specification that match a part of the URL. If a match occurs, the value in that part of the URL is supplied as a variable in the request object. This is used in the following form:
app.get('/customers/:customerId', ...
The URL /customers/1234 will match the above route specification, and so will /customers/4567. In either case, the customer ID will be captured and supplied to the handler function as part of the request in req.params, with the name of the parameter as the key. Thus, req.params.customerId will have the value 1234 or 4567 for each of these URLs, respectively.
It’s no coincidence that route parameters work really well for what we discussed as good REST API endpoints. This feature was designed just for that. In fact, you can have multiple parameters, for example /customers/:customerId/orders/:orderId, to match a customer’s order.
■Note the query string is not part of the path specification, so you cannot have different handlers for different parameters or values of the query string.
Route Lookup
Multiple routes can be set up to match different URLs and patterns. The router does not try to find a best match; instead, it tries to match all routes in the order in which they are installed. The first match is used. So, if two routes are possible matches to a request, it will use the first defined one. It is up to you to define routes in the order of priority. So, when you add patterns rather than very specific paths, you should be careful to add the more generic pattern after the specific paths. For example, if you want to match everything that goes under /api/, that is, a pattern like /api/*, you should add this route only after all the specific routes that handle paths such as /api/issues.
Handler Function
Once a route is matched, the handler function is called, which in the above example was an anonymous function supplied to the route setup function. The parameters passed to the handler are a request object and a response object. Let’s briefly look at the important properties and methods of these objects.
Request Objects
Any aspect of the request can be inspected using the request object’s properties and methods. You already saw how to access a parameter value using req.params. The other important properties follow.
Chapter 5 ■ express rest apIs
74
• req.query: This holds a parsed query string. It’s an object with keys as the query string parameters and values as the query string values. Multiple keys with the same name are converted to arrays, and keys with a square bracket notation result in nested objects (e.g., order[status]=closed can be accessed as req.query.
order.status).
• req.header, req.get(header): The get method gives access to any header in the request. The header property is an object with all headers stored as key-value pairs. Some headers are treated specially (like Accept), and have dedicated methods in the request object for them. That’s because common tasks that depend on these headers can be easily handled.
• req.path: This contains the path part of the URL, that is, everything up to any ? that starts the query string. Usually, the path is part of the route if the route is not a pattern, but if the path is a pattern that can match different URLs, you can use this property to get the actual path that was received in the request.
• req.url, req.originalURL: These properties contain the complete URL, including the query string. Note that if you have any middleware that modifies the request URL, originalURL will hold the URL as it was received, before the modification.
• req.body: This contains the body of the request, valid for POST, PUT, and PATCH requests. Note that the body is not available (req.body will be undefined) unless a middleware is installed to read and optionally interpret or parse the body.
There are many other methods and properties; for a complete list, please refer to the Request documentation of Express at http://expressjs.com/en/api.html#req as well as Node.js’ request object at https://nodejs.org/api/http.html#http_class_http_
incomingmessage, from which the Express Request is extended.
Response Objects
The response object is used to construct and send a response to a request. If no response is sent, the client is left waiting.
• res.send(body): You already saw the res.send() method briefly, which responded with a string. This method can also accept a buffer (in which case the content type is set as application/
octet-stream as opposed to text/html in case of a string). If the body is an object or an array, it is automatically converted to a JSON string with an appropriate content type.
Chapter 5 ■ express rest apIs
• res.status(code): This sets the response status code. If not set, it is defaulted to 200 OK. One common way of sending an error is by combining the status() and send() methods in a single call like res.status(403).send("Access Denied").
• res.json(object): This is the same as res.send(), except that this method forces conversion of the parameter passed into a JSON, whereas res.send() may treat some parameters like null differently. It also makes the code readable and explicit, stating that you are indeed sending out a JSON.
• res.sendFile(path): This responds with the contents of the file at path. The content type of the response is guessed using the extension of the file.
There are many other methods and properties in the response object; you can look at the complete list in the Express documentation for Response at http://expressjs.
com/en/api.html#res and also Node.js’ Response object in the HTTP module at https://nodejs.org/api/http.html#http_class_http_serverresponse. But for the Issue Tracker application, the methods I’ve discussed should suffice.
Middleware
Express is a web framework that has minimal functionality of its own. An Express application is essentially a series of middleware function calls. In fact, the Router itself is nothing but a middleware function.
Middleware functions are those that have access to the request object (req), the response object (res), and the next middleware function in the application’s request- response cycle. The next middleware function is commonly denoted by a variable named next. I won’t go into the details of how to write your own middleware functions, since we will not be writing new middleware in the application. But we will use some middleware for sure.
We already used one middleware called express.static in the Hello World example, to serve static files. This is the only built-in middleware (other than the router) available as part of Express. But there are other very useful middleware supported by the Express team, of which we will be using body-parser in this chapter. Third-party middleware is available via npm.
Middleware can be at the application level (that is, applies to all requests) or the router level (applies to specific request path patterns). The way to use a middleware at the application level is to simply supply the function to the application, like this:
app.use(middlewareFunction);
The actual middleware function is supplied by the module in a module-specific way. For example, the static middleware used a “factory” function to “manufacture” a middleware function. We supplied the factory function one parameter: the location of the static files. This created the real middleware function that knows the location of static files to serve.
Chapter 5 ■ express rest apIs
76
In order to use the same middleware in a route-specific way, you could have done the following:
app.use('/public', express.static('static'));
This would have mounted the static files on the path /public and all static files would have to be accessed with the prefix /public, for example, /public/index.html.
The List API
Now that you have learned the basic concepts of Express, let’s start by implementing the first API. This will be the List API, which lists all issues. We’re not integrating this with the front-end code just yet; instead we’ll test it by other means. We’ll directly call this API from the browser or the shell using curl. We also won’t use the version indicator in the APIs, since they are for internal consumption only.
Since the Issue Tracker application has a single resource for now, the endpoint or URI that we’ll use is straightforward: /api/issues. All the List API has to do is return the complete list of issues.
We will store the list of issues in your server’s memory, by moving the global array from the file App.jsx to the server. Then, we will add a get route to the application, which just sends out this global array of issues as a JSON using res.json().
The new file, server.js, is shown in Listing 5-1.
Listing 5-1. server.js: New List API const express = require('express');
const app = express();
app.use(express.static('static'));
const issues = [ {
id: 1, status: 'Open', owner: 'Ravan',
created: new Date('2016-08-15'), effort: 5, completionDate: undefined, title: 'Error in console when clicking Add',
}, {
id: 2, status: 'Assigned', owner: 'Eddie', created: new Date('2016-08-16'), effort: 14,
completionDate: new Date('2016-08-30'), title: 'Missing bottom border on panel', },
];
app.get('/api/issues', (req, res) => {
const metadata = { total_count: issues.length };
res.json({ _metadata: metadata, records: issues });
});
Chapter 5 ■ express rest apIs app.listen(3000, () => {
console.log('App started on port 3000');
});
Automatic Server Restart
In order to have the changes take effect, you need to restart the server. You can stop the server using Ctrl-C, and start it again using npm start. But, if you are going to make many changes and test them often, this soon becomes a hassle. It’s not just the act of restarting, but you’ll find yourself spending time debugging why something didn’t work and realize it was all because the server was not restarted!
To automatically restart the server on changes, let’s install and use the package nodemon. You may also find by searching the Internet that forever is another package that can be used to achieve the same goal. Typically, forever is used to restart the server on crashes rather than watch for changes to files. The best practice is to use nodemon during development (where you watch for changes) and forever on production (where you restart on crashes). So, let’s install nodemon now:
$ npm install nodemon --save-dev
Since it is a local install, you need to use a script in package.json that tells npm to use nodemon instead of running the server directly. You can either modify the start command or add a new command. We will just modify the start command, assuming that on production, we won’t be using npm start. Here are the changes in package.json:
...
"scripts": {
"start": "nodemon -w server.js server.js", ...
The –w command line option is to tell nodemon which files to watch for changes. If we hadn’t supplied that command line option, it would have watched for changes in any file in the current directory and subdirectories. Thus, it would have restarted even when front-end code changed, and that’s not what we want.
After the installation and changes to package.json, you can run npm start and see that any change to server.js automatically restarts the server. You can also change App.jsx and see that it does not restart the server, even when App.js is generated and saved in the static directory.
Testing
Now that you’ve restarted the server, let’s test the newly created API. To test it, you can just type http://localhost:3000/api/issues in the browser’s URL. Or, you can use the command line utility curl, which will come in handy while testing HTTP methods other than GET (because you can only simulate a GET by typing in the URL). It may also be
Chapter 5 ■ express rest apIs
78
useful to install browser extensions or apps such as getpostman (www.getpostman.com/), a full-fledged API tester. The extension jsonview is also useful to see JSON output formatted nicely in the browser.
You can choose your favorite tool, but I personally prefer using Chrome’s Developer Console to inspect the network traffic. It also automatically parses JSON and shows it as a collapsible tree. For the purpose of showing results in this book, I will show just a curl execution snippet. The curl command-line and the output of testing the List API are as follows:
$ curl -s http://localhost:3000/api/issues | json_pp {
"_metadata" : { "total_count" : 2 },
"records" : [ {
"id" : 1,
"title" : "Error in console when clicking Add", "effort" : 5,
"created" : "2016-08-15T00:00:00.000Z", "owner" : "Ravan",
"status" : "Open"
}, {
"title" : "Missing bottom border on panel", "created" : "2016-08-16T00:00:00.000Z", "effort" : 14,
"id" : 2,
"owner" : "Eddie", "status" : "Assigned",
"completionDate" : "2016-08-30T00:00:00.000Z"
} ] }
Let’s examine the interesting parts of the code. The entry point for the API is the Express route for the resource URI:
...
app.get('/api/issues', (req, res) => { ...
This code sets up a route such that a request to /api/issues (only GET requests, though) will be handled by the function that is defined here. The part of this function that returns a response is in the following line:
Chapter 5 ■ express rest apIs ...
res.json({ _metadata: metadata, records: issues });
...
The above is actually a shortcut for the following:
...
res.set('Content-Type', 'application/json');
res.send(JSON.stringify({ _metadata: metadata, records: issues }));
...
This essentially sends out a JSON string representation of the object that we passed to the function. In this case, the object consists of a metadata and the set of records.
For the set of records, we just used the in-memory array of issues. The metadata at the moment consists only of the total count of records. This can be expanded to add other useful metadata such as pagination information in the future.