Topic 1: Introduction to JSON Web APIs

Introduction

In this week's topic you will be introduced to the concept of web APIs. You will get an initial idea of why they are useful, and will start to write a simple web API using Node.js and Express.

Is HTML always the best output format?

Think about the work you did last year in Web Technologies, in which you wrote a simple Node/Express server to search a database table and display the results using EJS. You generated HTML as the output format, so that the results look good in a browser.

The problem with HTML, however, is that it is a format specific to a browser. This is fine if we are simply writing a website to be viewed by an end-user in a browser. But the end-user is not the only potential user of the data. Other applications might want to make use of the content, for example a weather app running on Android or iOS might want to access weather forecasts from a weather website. Apps do not typically use HTML, they use their own user interface, so it would be better if our server delivered the data as raw data, without any presentation or formatting. An example of a raw data format is JSON, which you saw last year in COM419. Raw data could then be easily processed by any front end, for example:

Web APIs - Introduction

A web API is an application running on a web server which provides raw data (such as JSON) to other applications (client applications), as we saw above. Web APIs receive HTTP requests from clients, process the request, and deliver the response back as an HTTP response - but unlike the simple servers you saw last year, the response is delivered as raw data, rather than HTML. This is shown on the diagram below:

JSON web APIs

Here are a few examples of web APIs and their clients:

Raw data is easier to parse

Web APIs obviously send back information to their clients, but as we have seen, a raw data format such as JSON is preferred to HTML. Why is this? HTML is not considered a good idea because it contains not only data, but also page structure information (headings, paragraphs, tables etc). A client website using the web API, or an app, might wish to arrange the information in a different way. We will look at this practically next week.

So what we want is a format which represents the data, and the data alone. There are a number of formats we can use, including JSON (JavaScript Object Notation), and also XML (eXtensible Markup Language). In this module we will focus on JSON, because it's the leading format, it is easy to generate on the server and to parse (interpret) on the client, and you have met it already.

JSON - JavaScript Object Notation - Revision

JSON uses JavaScript syntax (hence the name) to represent data as it gets sent across the web. As you saw last year, JavaScript uses curly brackets {} to represent objects (similar to Python dictionaries in the sense that they consist of key/value pairs, though you can also add methods to objects) and square brackets [] to represent arrays. So with JSON we reuse this syntax to represent data, using curly brackets {} to represent a single entity (such as a person, a song or a film) and square brackets [] to represent a collection of entities (i.e. an array of entities).

Here is an example of a JSON object representing a single student.

{ 
    "name": "Tim Smith",
    "username": "2smitt82",
    "course": "Computer Studies"
}
Note how the JSON object representing the student is defined by curly brackets { and }, and inside the curly bracket, we specify each property of the student (name, username and course) and the corresponding value ("Tim Smith", "2smitt82", and "Computer Studies", respectively). A colon (:) separates the property and the value, and a comma separates each property/value pair.

The next example shows a collection (array) of students. Note how we use the JSON array syntax [ and ] to define the collection, how each individual student object is represented by curly brackets { and }, and how each student object within the array is separated by a comma.

[
    { 
      "name": "Tim Smith",
      "username": "2smitt82",
      "course": "Computer Studies"
    },

    {
      "name": "Jamie Bailey",
      "username": "1bailj39",
      "course": "Computer Studies"
    },

    {
      "name": "Deep Patel",
      "username": "0pated61",
      "course": "Networks and Web Design"
    }
]

Node and Express - Revision

In other modules you have looked at Node.js and Express. This section is provided for revision. Node.js can be downloaded from the website.

npm

As you have seen, Node also comes with a package manager called npm. This allows you to use add-ons to Node (packages) which perform additional tasks, not part of the core Node.js distribution, such as communicating with a database. To use npm to install new packages, you enter:

npm install <packagename>
at the command-line (e.g. DOS or Linux shell prompt).

API documentation

See here for full Node API documentation.

Creating a Node web server Using Express

It is possible to create a webserver from first principles in Node, using the HTTP module. However, as you have seen in previous modules, it's easier to use a pre-existing web server framework, and Express is perhaps the most widely used. The npm command below will install it:

npm install express
This will install it locally to your current project. Or to install it globally so that it's accessible anywhere on your machine, for all projects and all users:
npm install -g express
On Linux and other Unix-based systems, this requires 'sudo' rights (admin privileges).

Hello World with Express

The example below is the Hello World with Express. Note that there are two ways of including third-party modules such as Express: CommonJS modules and ECMAScript 6 modules. The latter is the currently-recommended approach and is a standard part of recent JavaScript. The difference when coding is that you use require() with CommonJS modules, but import with ECMAScript 6 modules. Note that if you use ECMAScript 6 modules in Node.js, you must save the file with a .mjs extension, not .js..

// app.mjs - use .mjs extension as ECMAScript 6 modules are used

import express from 'express';
const app = express();

app.get('/', (req,res)=> {
    res.send('Hello World from Express!');
});

app.listen(3000);
We import the express module, then create an Express app object with express().

With Express, we set up routes. A route is a combination of a particular URL and a handler function for that URL. The URLs for a particular API are called API endpoints. So a route can be described as a handler for a particular endpoint, or set of endpoints (we can set up a single route to handle multiple URLs). In this example we are simply handling the top level, 'root' endpoint, so to request it from a browser, we would enter:

http://localhost:3000/
without any parameters supplied.

The handler for the endpoint is a function which takes two parameters, req representing the HTTP request and res representing the HTTP response. In this example we call the send() method of the response object to send back Hello World from Express! to the client.

Here is an example of an Express server with two routes:

// app.mjs - use .mjs extension as ECMAScript 6 modules are used

import express from 'express';
const app = express();

app.get('/', (req,res)=> {
    res.send('Hello World from Express!');
});

app.get('/time', (req, res) => {
	res.send(`There have been ${Date.now()} milliseconds since 1/1/70.`);
});

app.listen(3000);
There are two routes, a default (or root route) which sends back a Hello World message, and a /time route which displays the number of milliseconds since the start of 1970.

These would be accessed with:

http://localhost:3000
for the root route, and
http://localhost:3000/time
for the time route.

Running the server

Remember that you use the node command to run a Node application. So if the above basic Express app is saved as app.mjs, you would run it with:

node app.mjs
This will run the server continuously in the background. To stop the server, you need to press Control-C. You must do this if you change the server code as the server will not automatically reload if you change it!

Node and SQLite

Downloading and using SQLite

Last year you used the SQLite relational database: if you remember, SQLite uses flat files rather than working with a full client-server database system. You can download SQLite here. You can query and manipulate an SQLite database via the console-based sqlite3 tool, documented here and downloadable here along with the SQLite libraries. You can also upload a .db database to the online "fiddle" tool here and enter SQL statements to query and manipulate the database.

SQLite Studio allows you to manage an SQLite database graphically. A web-based alternative is mysqlview which also works with SQLite.

Using SQLite from Node.js

We are going to look now at how you can connect to an SQLite database from Node.js. There are various modules available to do the job, including better-sqlite3 which we will use as it is straightforward and, according to its developers, gives better performance than some alternatives. Documentation is available here

To install, use npm as for last week from the command prompt, e.g:

npm install better-sqlite3 

Here is a simple example. You need to import the Database class from the better-sqlite3 module and then create a Database object using your .db file:

import Database from 'better-sqlite3';
const db = new Database('mydatabase.db'); 

// Rest of the code follows...

Web API Development with Node and Express

Having revised some topics from last year we will now focus on how we can develop Web APIs with Node and Express. As we saw above, Web APIs are in fact very similar to standard web applications - but they deliver a pure data format, such as JSON, back to the client.

Individual routes within a Web API are known as API endpoints. So we could have one API endpoint for looking up all songs by a particular artist, one API endpoint for buying a song, one API endpoint for adding a new song, and so on.

REST - Representational State Transfer - An introduction

You should revise HTTP requests and responses from OODD before looking at this topic.

It is a very common pattern in web development to develop a web API with a series of endpoints which manipulate the database in various ways, using HTTP methods appropriately for different operations. For example, if we are searching the database with a SELECT query, we use a GET method. If we are updating or inserting data, we use a POST method (or a PUT method - we will cover these next week) and if we are deleting data, we use a DELETE method. We can set these methods up in Express easily:

app.get('/endpoint1', ....);    // GET request
app.post('/endpoint2', ....);   // POST request
app.delete('/endpoint3', ....); // DELETE request
We can also use various HTTP methods from within our Express server to signal different types of error, e.g. We can send back different HTTP status codes from Express with the status() method, eg.:
res.status(404);
This pattern, of: is a key feature of the REST (Representational State Transfer) pattern for developing web APIs. REST has other key principles such as statelessness; we will look at these in more detail next week.

Writing API endpoints which communicate with SQLite using the better-sqlite3 module

We will now go through a series of example API endpoints using various methods, and which communicate with an SQLite database. You have already worked with SQLite databases from Node and Express. This year we are using a different module, better-sqlite3, but the style of code is similar.

SELECT

The code below is a snippet of code showing an API endpoint as part of an Express REST Web API. It perfoms an SQL statement to find all students with the last name matching the lastname parameter to the Express route, so that for example:

https://url-of-your-server.example.com/students/Smith
would find all students with the last name Smith.
app.get('/students/:lastname', (req, res) => {
    try {
        const stmt = db.prepare("SELECT * FROM students WHERE lastname=?");
        const results = stmt.all(req.params.lastname);
        res.json(results);
    } catch(error) {
        res.status(500).json({ error: error });
    }
});

We first create a prepared statement using the prepare() method of our database object. A prepared statement is a statement which has parameters bound to it, and then is compiled into a binary form which can be stored in memory and rapidly executed by the database. The advantage of a prepared statement is that once compiled, it can be rapidly executed again and again by the database if our application performs the same query multiple times. Prepared statements also prevent a certain type of security exploit known as SQL injection, in which a database can be compromised by the user entering fragments of SQL into a form which combine with existing SQL code in the JSP. We will look at SQL injection in more detail later in the module.

  • Note how the SQL contains placeholders for bound parameters, using question marks ? We bind data to each placeholder when we execute the statement. To execute a SELECT statement we use the all() method of our prepared statement object, and pass the bound parameters as arguments. So here:
    const results = stmt.all(req.params.lastname);
    will bind the lastname parameter of the route to the first placeholder. The result will be that we query the database for all students with that last name.

    The all() method returns an array of all matching rows from the database, as an array of JavaScript objects. We send back that array as JSON, using res.json(), which we saw last week.

    Note also the use of a try/catch block. Those of you who have studied OODD (which I believe is everyone this year) will have seen this already. Basically we are trying to do something, and if it fails, we catch the error (exception) in the catch block. So, if there was an error, we send back a JSON object containing an error property with a value of the error that occurred, and also send back an HTTP status code of 500 (Internal Server Error) which, by convention, is used if the server encountered an internal error which was not caused by a user mistake.

    This version returns all students in the database. Note that in this case, we don't need to specify a parameter to the the query, so stmt.all() takes no arguments.

    app.get('/students', (req, res) => {
        try {
            const stmt = db.prepare("SELECT * FROM students");
            const results = stmt.all();
            res.json(results);
        } catch(error) {
            res.status(500).json({ error: error });
        }
    });
    

    UPDATE

    Performing an UPDATE statement uses the same approach. Here is a route which could be used to buy a product with a given ID by reducing its quantity in stock by one. Note how this route has a method of POST, following the REST principles discussed above.

    app.post('/products/:id/buy', (req, res) => {
        try {
            const stmt = db.prepare('UPDATE products SET quantity=quantity-1 WHERE id=?');
            const info = stmt.run(req.params.id);
            if(info.changes == 1) {
                res.json({success:1});
            } else {
                res.status(404).json({error: 'No product with that ID'});
            }
        } catch(error) {
            res.status(500).json({ error: error });
        }
    });
    
    Note that we use run(), rather than all(), with a statement which updates the database. We use a try/catch block, as for the SELECT example. Also, we check the changes property of the info object returned from the query. This contains the number of rows affected by the SQL statement. If one row was updated, we send back a JSON success message. If not, the reason will be that there is no product with that ID. By convention, following the principles of REST, if we cannot find what we are looking for in a route, we send back the HTTP status code 404 (Not Found).

    Note how the route contains the ID as a parameter (:id). You looked at this last year: parameters allow you to pass information into the route via the URL. So for example, the URL:

    http://localhost:3000/products/201/buy
    would pass a parameter of 201 into the route. The parameter :id would have the value 201. Also remember how we use req.params to access parameters, so req.params.id represents the :id parameter.

    DELETE

    DELETE is similar to UPDATE. As we saw above, a route which results in the deletion of data makes use of the HTTP DELETE method.

    app.delete('/products/:id', (req, res) => {
        try {
            const stmt = db.prepare('DELETE FROM products WHERE id=?');
            const info = stmt.run(req.params.id);
            if(info.changes == 1) {
                res.json({success:1});
            } else {
                res.status(404).json({error: 'No product with that ID'});
            }
        } catch(error) {
            res.status(500).json({ error: error });
        }
    });
    
    Note how again we check the changes property of the results to check whether it found the record to delete. Note also how the DELETE route takes in the ID as a parameter.

    Testing HTTP request methods other than GET

    One question remains. How can you test out these methods? With GET methods, it's simple; you simply request the desired API endpoint in your browser and the appropriate route will be executed. However, with other methods, such as POST and DELETE, it's more difficult, as browsers always send GET requests when you type in a URL.

    Option One - RESTer

    There are various third-party tools which can be used to test out APIs. A particularly useful one is RESTer, which is strongly recommended if using your own computer. However please note this is not available on the university computers and cannot be installed due to the controlled environment. This is a browser extension which allows you to simulate HTTP requests and is available for Chrome and Firefox. To install RESTer, visit either of these links and follow the instructions, and RESTer will be installed to your browser.

    RESTer has an interface as shown below:
    RESTer interface
    Note how RESTer allows you to specify, amongst other things:

    In the example above, a GET request is being made to the URL https://url_of_your_server.example.com/artist/Oasis. Note how the response is shown below the controls, showing the status code, the response headers and the response body.

    RESTer also allows you to test error conditions: if you supply invalid request data and your web API checks for this, then you will get a non-200 status code returned.

    Option Two - REST Tester

    If you are on the university computers, you will be unable to use RESTer, but can use an alternative - more basic - tool called REST Tester. This can be downloaded from GitHub:

    git clone https://github.com/nickw1/resttest.git
    Download this, change to its installation folder:
    cd resttest
    and then install the dependencies:
    npm install
    and run, e.g.:
    node app.mjs
    or use PM2 (below). The application will run on port 3200, so you can access via:
    http://localhost:3200
    It is shown below:
    REST Tester

    Exercises

    Important!As we need to devote part of the lab session in Week 1 to the introductory lecture, it is possible that you will not finish these, so time is allocated in Week 2 for you to finish them. In subsequent weeks the arrangement will bt the same as OODD: the background behind each topic will be covered in a lecture, which can be viewed as a live Teams session (time TBC) or as a recording in your own time.

    1. Revision/checking that your environment works: Create the simple Hello World Express example above. Run it, and request it from your browser to show that it works. Then, add :
      • the /time route shown above to your Express server and test that.
      • a /greet route which takes a name as a parameter and responds with "Hello" plus the name supplied.
    2. Use this db file for your database. Develop a simple REST API in Node and Express with endpoints to:
      • GET: search for all songs by a given artist;
      • GET: search for all songs with a given title;
      • GET: search for songs by artist AND title (i.e. both must match);
      • GET: find a song with a given ID
      • POST: buy a physical copy of song
      • DELETE: delete a song with a given ID
    3. Test the GET endpoints in the browser, and the POST and DELETE endpoints in RESTer, if on your own computer, or with REST Tester if not.

    Keeping a Node app running - PM2 - Node process manager

    A useful thing to know is how to keep a Node app running when it has been started. This is obviously important on a live server, where we will want to restart the app if it crashes for any reason. So far, you have just started Node apps from the command-line, e.g:

    node app.mjs
    and then stopped then with Control-C. Obviously though, this is not satisfactory for a production app, which needs to be running all the time.

    So we need some kind of software to keep our Node server running continuously. There are various options available, including Nodemon, together with the software we will examine, pm2 (see here). pm2 is a process manager typically used with Node applications, though it can also be used to manage other background processes. With pm2, you can launch a Node process in the background, stop it, start it and monitor how it is running. Furthermore if the process crashes, pm2 will restart it, which is again essential on a production server.

    It can be installed with npm. Unlike the other NPM packages we have looked at so far, you will want to install it globally so that it is available to all applications and all users on your system. For example this command will install it globally for all users if you are logged in as a user with admin privileges on a Linux system (sudo means "run with root, or admin privileges", and -g indicates to install it globally):

    sudo npm install -g pm2
    Or, if you do not have admin privileges, you can make pm2 available to all applications owned by your user by omitting the sudo:
    npm install -g pm2

    Using pm2

    To start a given Node server with pm2 you use this command:

    pm2 start name_of_file.mjs
    Here is an example. Note the cat command on a Linux system shows the contents of the file, this has been done to show the source code of ths server:
    Starting a process with pm2
    As can hopefully be seen, this server runs on port 3000, and we have started it. Note that it has been allocated a process ID of 0, which we can use later to stop the process.

    The screenshot below shows starting a second process. Note that the second server runs on a different port, 3001, which allows both processes to run at the same time.
    Starting a second process with pm2
    Once we've started a process, we can then stop it using

    pm2 stop process_name_or_process_ID
    So in the example below, we stop the second process, using its process ID (1):
    Stopping a process using its ID
    We can then restart the process. In the example below, we restart the second server using its process name, rather than its ID:
    Restarting a process using its name
    We can completely remove a process from the list with pm2 delete. So the example below stops, and completely removes from the list, the second server.
    Deleting a process
    The next example shows how to start a process with a custom name (SecondServer). This is done using the --name option to pm2.
    Starting a process with a custom name
    Note how the second server has an ID of 2. This is because it is a brand new process: process 1 was deleted and removed from the list.

    Watch mode

    The above example also shows how to start a process in watch mode. Wtch mode will automatically restart the server if changes are made, which is very useful during development, as you do not have to keep stopping and restarting your server if you make changes. As can be seen in the example, the --watch option is used to add the process in watch mode:

    pm2 start server2.mjs --watch --name SecondServer

    Listing all processes

    You can also list all processes managed with pm2 without having to perform another operation. Just use pm2 list, e.g.:
    Listing all processes