A Complete Server: Getting Started
We're ready to start building a complete server API. We're going to build a production e-commerce app that can be adapted to be used as a starting place for almost any company.
Soon, we'll cover things like connecting to a database, handling authentication, logging, and deployment. To start, we're going to serve our product listing. Once our API is serving a product listing, it can be used and displayed by a web front-end. However, we're going to build our API so that it can also be used by mobile apps, CLIs, and programmatically as a library.
Our specific example will be for a company that sells prints of images -- each product will be an image. Of course, the same concepts can be applied if we're selling access to screencasts, consulting services, physical products, or ebooks. At the core, we're going to need to be able to store, organize, and serve our product offering.
Getting Started#
To get started with our app, we'll need something to sell. Since our company will be selling prints of images, we'll need a list of images that are available for purchase. We're going to use images from Unsplash.
Using this list of images, we'll build an API that can serve them to the browser or other clients for display. Later, we'll cover how our API can handle purchasing and other user actions.
Our product listing will be a JSON file that is an array of image objects. Here's what our products.json
file looks like:
[
{
"id": "trYl7JYATH0",
"description": "This is the Department and Water and Power building in Downtown Los Angeles (John Ferraro Building).",
"imgThumb": "https://images.unsplash.com/photo...",
"img": "https://images.unsplash.com/photo...",
"link": "https://unsplash.com/photos/trYl7JYATH0",
"userId": "X0ygkSu4Sxo",
"userName": "Josh Rose",
"userLink": "https://unsplash.com/@joshsrose",
"tags": [
"building",
"architecture",
"corner",
"black",
"dark",
"balcony",
"night",
"lines",
"pyramid",
"perspective",
"graphic",
"black and white",
"los angeles",
"angle",
"design"
]
},
]
As we can see, each product in the listing has enough information for our client to use. We can already imagine a web app that can display a thumbnail for each, along with some tags and the artist's name.
Our listing is already in a format that a client can readily use, therefore our API can start simple. Using much of what we've covered in chapter 1, we can make this entire product listing available at an endpoint like http://localhost:1337/products
. At first, we'll return the entire listing at once, but quickly we'll add the ability to request specific pages and filtering.
Our goal is to create a production-ready app, so we need to make sure that it is well tested. We'll also want to create our own client library that can interact with our API, and make sure that is well tested as well. Luckily, we can do both at the same time.
We'll write tests that use our client library, which in turn will hit our API. When we see that our client library is returning expected values, we will also know that our API is functioning properly.
Serving the Product Listing#
At this stage, our expectations are pretty clear. We should be able to visit http://localhost:1337/products
and see our product listing. What this means is that if we go to that url in our browser, we should see the contents of products.json
.
We can re-use much of what we covered in Chapter 1. We are going to:
Create an
express
serverListen for a GET request on the
/products
routeCreate a request handler that reads the
products.json
file from disk and sends it as a response
Once we have that up, we'll push on a bit farther and:
Create a client library
Write a test verifying that it works as expected
View our products via a separate web app
Express Server#
To start, our express
server will be quite basic:
const fs = require('fs').promises
const path = require('path')
const express = require('express')
const port = process.env.PORT || 1337
const app = express()
app.get('/products', listProducts)
app.listen(port, () => console.log(`Server listening on port ${port}`))
async function listProducts (req, res) {
const productsFile = path.join(__dirname, '../products.json')
try {
const data = await fs.readFile(productsFile)
res.json(JSON.parse(data))
} catch (err) {
res.status(500).json({ error: err.message })
}
}
Going through it, we start by using require()
to load express
and core modules that we'll need later to load our product listing: fs
and path
.
const fs = require('fs').promises
const path = require('path')
const express = require('express')
Next, we create our express
app, set up our single route (using a route handler that we will define lower in the file), and start listening on our chosen port (either specified via environment variable or our default, 1337
):
const port = process.env.PORT || 1337
const app = express()
app.get('/products', listProducts)
app.listen(port, () => console.log(`Server listening on port ${port}`))
Finally, we define our route handler, listProducts()
. This function takes the request and response objects as arguments, loads the product listing from the file system, and serves it to the client using the response object:
async function listProducts (req, res) {
const productsFile = path.join(__dirname, '../products.json')
try {
const data = await fs.readFile(productsFile)
res.json(JSON.parse(data))
} catch (err) {
res.status(500).json({ error: err.message })
}
}
Here, we use the core path
module along with the globally available __dirname
string to create a reference to where we are keeping our products.json
file. __dirname
is useful for creating absolute paths to files using the current module as a starting point.
Unlike
require()
,fs
methods likefs.readFile()
will resolve relative paths using the current working directory. This means that our code will look in different places for a file depending on which directory we run the code from. For example, let's imagine that we have created files/Users/fullstack/project/example.js
and/Users/fullstack/project/data.txt
, and inexample.js
we hadfs.readFile('data.txt', console.log)
. If we were to runnode example.js
from within/Users/fullstack/project
, thedata.txt
would be loaded and logged correctly. However, if instead we were to runnode project/example.js
from/Users/fullstack
, we would get an error. This is because Node.js would unsuccessfully look fordata.txt
in the/Users/fullstack
directory, because that is our current working directory.
__dirname
is one of the 21 global objects available in Node.js. Global objects do not need us to userequire()
to make them available in our code.__dirname
is always set to the directory name of the current module. For example, if we created a file/Users/fullstack/example.js
that containedconsole.log(__dirname)
, when we runnode example.js
from/Users/fullstack
, we could see that__dirname
is/Users/fullstack
. Similarly, if we wanted the module's file name instead of the directory name, we could use the__filename
global instead.
After we use fs.readFile()
, we parse the data and use the express
method res.json()
to send a JSON response to the client. res.json()
will both automatically set the appropriate content-type header and format the response for us.
If we run into an error loading the data from the file system, or if the JSON is invalid, we will send an error to the client instead. We do this by using the express
helper res.status()
to set the status code to 500 (server error) and send a JSON error message.
This page is a preview of Fullstack Node.js