In Chapters 10 and 11, you consumed external APIs using fetch. You might have wondered how these APIs were created. While you could use any server-side technology (the ones on randyconnolly.com were in fact created using PHP) to implement an API, Node is a particularly popular technology for doing so.
Most REST APIs are HTTP front-ends for querying a database. As such, you will learn how to create a database-driven API in the next chapter on databases. Nonetheless, we can still demonstrate how to implement an API in Node by reading in a JSON file and use that as our data source.
Listing 13.3 provides the code for a very simple API. It reads in a JSON data file and then returns the JSON data when the URL is requested.
// first reference required modules
const fs = require('fs');
const path = require('path');
const express = require('express');
const app = express();
// for now, we will read a json file from public folder
const jsonPath = path.join(__dirname, 'public', 'companies.json');
// get data using conventional Node callback approach
let companies;
fs.readFile(jsonPath, (err,data) => {
if (err)
console.log('Unable to read json data file');
else
companies = JSON.parse(data);
});
// return all the companies when a root request arrives
app.get('/', (req,resp) => { resp.json(companies) } );
// Use express to listen to port
let port = 8080;
app.listen(port, () => {
console.log("Server running at port= " + port);
});Notice the emphasized code in Listing 13.3. It uses the conventional Node callback approach. Node predates Promises and async...await by almost a decade, so most Node packages make use of callback functions. By convention, many Node callback functions take two parameters: an error object and a data object. In this case, the data object in the readFile() callback will contain the content of the file.
Since Node v11 (late 2018), Node has had async...await support as well as promisified versions of many of its built-in packages as well. You could eliminate the callback in Listing 13.3 and use async...await as shown in the following:
// use the promisified version of the fs package
const fs = require('fs').promises;
...
let companies;
getCompanyData(jsonPath);
...
async function getCompanyData (jsonPath) {
try {
const data = await fs.readFile(jsonPath, "utf-8");
companies = JSON.parse(data);
}
catch (err) {
console.log('Error reading ' + jsonPath);
}
}
To make the web service created in the previous section more useful, let’s add some additional routes. Recall that in Express, routing refers to the process of determining how an application will respond to a request. For instance, instead of displaying all the companies, we might only want to display a single company identified by its symbol, or a subset of companies based on a criteria. These different requests are typically distinguished via different URL paths (instead of using query string parameters).
Let’s add two new routes: /companies/:symbol (which will return the JSON for a single company object that matches the supplied stock symbol) and /companies/name/:substring, which will return all companies whose name contains the supplied substring.
Adding new routes is simply a matter of adding app.get() calls for each route. Listing 13.4 illustrates the implementation of all three routes in the API.
// return all the companies if a root request arrives
app.get('/', (req,resp) => { resp.json(companies) } );
// return just the requested company, e.g., /companies/amzn
app.get('/companies/:symbol', (req,resp) => {
// change user supplied symbol to upper case
const symbolToFind = req.params.symbol.toUpperCase();
// search the array of objects for a match
const matches =
companies.filter(obj => symbolToFind === obj.symbol);
// return the matching company
resp.json(matches);
});
// return companies whose name contains the supplied text,
// e.g, /companies/name/dat
app.get('/companies/name/:substring', (req,resp) => {
// change user supplied substring to lower case
const substring = req.params.substring.toLowerCase();
// search the array of objects for a match
const matches = companies.filter( (obj) =>
obj.name.toLowerCase().includes(substring) );
// return the matching companies
resp.json(matches);
});You might have wondered why the Express routing function in Listing 13.4 is named get()? The explanation is quite straightforward. You use app.get() for HTTP GET requests, app.post() for POST requests, app.put() for PUT requests, and app.delete() for DELETE requests.
While the code in Listing 13.4 is relatively straightforward, what if we had five or six or more routes? In such a case, our single Node file would start becoming too complex. A better approach would be to separate out the routing functionality into separate modules.
A module in the traditional CommonJS approach in Node is similar to how you created modules in Chapter 10, except rather than using the JavaScript export keyword, you instead set the export property of the module object. Listing 13.5 illustrates how you could put the functionality for reading the JSON data for our API into a separate module. Notice the last line in the listing. It specifies the objects that will be available outside of this module; since the function getCompanyData() is not included in the list of exported objects, it is private to the module.
...
const fs = require('fs').promises;
// for now, we will get our data by reading the provided json file
const jsonPath = path.join(__dirname, '../public', 'companies.json');
// get data asynchronously
let companies;
getCompanyData(jsonPath);
async function getCompanyData(jsonPath) {
try {
const data = await fs.readFile(jsonPath, "utf-8");
companies = JSON.parse(data);
}
catch (err) { console.log('Error reading ' + jsonPath); }
}
function getData() {
return companies;
}
// specifies which objects will be available outside of module
module.exports = { getData };How do you make use of this module? Like any Node module, you need to use the require() function. For instance, if this code in Listing 13.5 was saved in a file named company-provider.js in the scripts subfolder, you could make use of it via the following lines of code:
const companyProvider = require('./scripts/company-provider.js');
...
const data = companyProvider.getData();
You could also place your route handler logic into a separate module. Listing 13.6 provides an illustration of how the route handlers in Listing 13.4 can look like in a module (we will save it in scripts folder as company-router.js).
// return all companies
const handleAll = (companyProvider, app) => {
app.get('/companies/', (req,resp) => {
// get data from company provider
const companies = companyProvider.getData();
resp.json(companies);
} );
}
// return just the requested company
const handleSingleSymbol = (companyProvider, app) => {
app.get('/companies/:symbol', (req,resp) => {
const companies = companyProvider.getData();
const symbolToFind = req.params.symbol.toUpperCase();
const stock = companies.filter(obj => symbolToFind === obj. symbol);
if (stock.length > 0) {
resp.json(stock);
} else {
resp.json(jsonMessage(`Symbol ${symbolToFind} not found`));
}
});
};
// return all the company whose name contains the supplied text
const handleNameSearch = (companyProvider, app) => {
app.get('/companies/name/:substring', (req,resp) => {
const companies = companyProvider.getData();
const substring = req.params.substring.toLowerCase();
const matches = companies.filter( (obj) =>
obj.name.toLowerCase().includes(substring) );
if (matches.length > 0) {
resp.json(matches);
} else {
resp.json(jsonMessage(
`No company matches found for ${substring}`));
}
});
};
const jsonMessage = (msg) => {
return { message: msg };
};
module.exports = {
handleAll,
handleSingleSymbol,
handleNameSearch
};How would these route handlers in a module be used? Listing 13.7 illustrates this, and also illustrates how these route handlers would be used. It also illustrates how to integrate static file handling and custom 404 handling for unknown routes.
const path = require('path');
const express = require('express');
const app = express();
// reference our own modules
const companyProvider = require('./scripts/company-provider.js');
const companyHandler = require('./scripts/company-router.js');
// handle requests for static resources
app.use('/static', express.static(path.join(__dirname, 'public')));
companyHandler.handleAll(companyProvider, app);
companyHandler.handleSingleSymbol(companyProvider, app);
companyHandler.handleNameSearch(companyProvider, app);
// for anything else, display 404 errors
app.use( (req,resp) => {
resp.status(404).send('Unable to find the requested resource!');
});
// use port in .env file or 8080
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log("Server running at port= " + port);
});Create a new Node API using Express for the supplied photos.json file.
Create a new file named test-know1.js. You will be implementing three routes.
The first route will be "/": it should return all the photo objects in the JSON file.
The second route will be "/:id" (e.g., /30): it should return just a single photo based on the id property in the file. If the supplied id value doesn’t exist in the file, return a JSON that contains an appropriate error message.
The third route will be "/iso/:iso" (e.g., /iso/ca): it should return all the photos whose iso property matches the supplied iso value. It should work the same for lowercase and uppercase iso values. If the supplied iso value doesn’t exist in the file, return a JSON that contains an appropriate error message.
Add static file handling to test-know1.js. To make it easier to test your routes, create a simple html file named test-know1.html in your public folder. This HTML file should contain a link to each of these routes.