This is the third installment of this Starter Web App series I’m doing. In part 2 we made our app server respond with different formats based on headers the client sends. Now we’re going to use dynamic data!
If you’re still on lesson 2, this will be cheating to continue. Spoiler alert!
This is about what we should have had from lesson 2.
const express = require('express') const app = express() app.get('/', (req, res) => { if(req.accepts('text/html')) { res.set('Content-Type', 'text/html') res.send('\ <html>\ <body>\ Welcome to the official site for <em>justified</em>\ <strong>cat hatred</strong>.\ </body>\ </html>') } else { res.set('Content-Type', 'text/plain') res.send('ohai') } }) app.listen(3000, () => console.log('Example app listening on port 3000!'))
1. providing data
Let’s add an endpoint (request handler) that gets us a list of cats. We’re going to model our endpoints after REST. I have a post that gives a very high level overview of REST, which you can read but isn’t required to move forward. The real basic gist is that with REST, our endpoints are modeled as noun-like things, and we have a very small number of fixed verb-like things we can do with the noun-like things.
Our endpoint for cats will simply be /cats
.
app.get('/cats', (req, res) => { res.send([ 'Chester', 'Foof', 'Garfield', 'Hobbes', 'Puss', 'Tom', 'Tony', 'Whiskers', ]) })
Doing a GET
on /cats
will provide a JSON list of cat names back. These are
all very legitimate cat names. I checked.
Let’s fire our server up and verify we can actually see these.
$ curl http://localhost:3000/cats ["Chester","Foof","Garfield","Hobbes","Puss","Tom","Tony","Whiskers"]
Whoa, it’s like the exact same set of strings we passed to res.send
! Spooky.
2. making the data easy to change
Okay let’s get rid of “Foof” because I only know one person who has a Foof cat and while that’s kind of neat name it’s pretty dumb of me to treat that as data. We’ll change “Foof” to “Mr. Druthersworth” because that’s a badass cat name.
I’ll wait until you get that done and test it with curl
. Don’t forget to
restart the node process when you make a change!
This is pretty stupid. Any time we want to change the data, we have to change our program and restart it. That’s because we’re not really changing data. We’re changing (source) code. To make it use real data, we should read it from a data source.
2.1. fs
fs
is a module built into Node.js that handles interacting with the file
system (hence “fs”). Computers are mostly fancy machines that present files.
Remember this. We’re going to put our cat name list into a file.
Let’s make a JSON file. JSON stands for JavaScript Object Notation. The format that JSON uses isn’t actually JavaScript but it’s very close. It’s incredibly picky compared to JavaScript. Notably different from what we’ve written thus far, it can’t use single quotes (everything is double quoted if it’s a string), and there can be no trailing comma. A trailing comma is a comma presented at the end of some comma separated sequence. Trailing commas are boss because you don’t have to think about making sure you remove that last comma or adding one should you rearrange a list. But we’re doing JSON so fuck you. One last notable thing about JSON is that you can copy and paste JSON directly into JavaScript and it will parse just fine. Not all JavaScript works as JSON though.
[ "Chester", "Mr. Druthersworth", "Garfield", "Hobbes", "Puss", "Tom", "Tony", "Whiskers" ]
Wow, this looks like the exact same thing our curl
output has, except
broken into multiple lines! That’s because it is. The line breaks are not of
consequence so long as it doesn’t happen inside a string. Let’s save this
file as cats.json
.
Now we’ll make our server load this file. First, we’ll need to require fs
.
Instead of giving you the syntax this time, I’m going to let you look it up.
The JS file you have right now has a great require
example. My suggestion
is to keep your require
calls all in the same spot, and towards the top of
the file before real logic begins. This part isn’t critical, but I find it
easier to find my way around: I sort the statements alphabetically. So the
require
call for express
would come before fs
. It makes things easier
to find. Some engineers like to group things together loosely by some kind of
arbitrary association (here goes the web things, here goes the file things,
etc), but these loose associations don’t actually communicate anything and
rarely do things fall into such nice and neat buckets.
Now that we have fs
, we can call fs.readFile
. readFile
takes a path to
a file, optional options (as in you don’t need to provide the options, they
are optional…), and a callback. The callback is a function we give
readFile
, and readFile
itself will call this function once the file has
been loaded. The function is supposed to take an error and data as two
parameters. That’s a pretty simple rundown of the function. You can see more
details from the documentation on readFile here if you like, but we can go
with my summary for now. For the path, we will use a relative path of
'./cats.json'
. For the options, we can give it 'utf8'
. The callback
needs to take an error
and data
parameters. If readFile
was successful,
error
will be null
. This is an exercise I will leave for you. The
documentation has an example of how to use the callback. Be sure this code
goes inside of your get
handler. This is because we need it to re-run the
file load every time we get a request. We can save the file and it will cause
a reload of the file. We won’t need to restart!
Once we have the callback setup, we need to do our due diligence and handle
the error condition. Handling errors is not extra stuff - it’s part of your
job. Later, we’ll go over tricks to make this an automatic process, but for
now it will be a matter of your personal discipline. If there’s an error, we
don’t want to throw
it like the documentation shows. We’re going to respond
to the client with an error code. Generally, we can respond with anything in
the 5xx range because loosely the 5xx range means that the server screwed up
somehow. 500 is a general catch all, and we should use that.
The general code for sending a 500 looks like this:
res.send(500, 'Could not load cat data!')
2.2. parsing
If data loading worked (the error
variable is null
), we can use it now as
a big string. That’s part of what 'utf8'
gives us. Without it, data
would
be a Buffer
which is something to look at another day.
We’re going to take that string and pass it to this global thing that just
hangs around: JSON.parse
. JSON
is a global variable and parse
is a
function on it. parse
takes a string and returns an object that the JSON
represents. Pass parse
your data, and out will come the cats. You can
then do res.send
on the cats.
I’m intentionally leaving things a little looser here so the work is on your end. Remember that even though our new setup will let us change the JSON file without reloading the server, we’ll still need to reload the server for any code changes.
3. cheats!
I strongly recommend struggling a little bit, but if you get really stuck, this section below should get you by. Part of the process is figuring this stuff out as you go. Senior Engineers are constantly googling things and visiting Stack Overflow for answers, documentation, known issues, etc. Anything that can help them move past particular problems. There is no shame in looking stuff up. If you can’t use Google, it’s honestly a valid reason to go home for the day. But all that said, sometimes it’s time to just ask for help.
const express = require('express') const fs = require('fs') const app = express() app.get('/', (req, res) => { if(req.accepts('text/html')) { res.set('Content-Type', 'text/html') res.send('\ <html>\ <body>\ Welcome to the official site for <em>justified</em>\ <strong>cat hatred</strong>.\ </body>\ </html>') } else { res.set('Content-Type', 'text/plain') res.send('ohai') } }) app.get('/cats', (req, res) => { fs.readFile('./cats.json', 'utf8', (error, data) => { if(error) { res.send(500, 'Could not load cat data!') } else { const cats = JSON.parse(data) res.send(cats) } }) }) app.listen(3000, () => console.log('Example app listening on port 3000!'))
4. moving on
The next part is learning about git so you can have both a backup, a log of changes, and a standard means of sharing your code while also viewing code others have shared as well.