Express
Express
Express
{
"author": "Jonathan Lee Martin",
"category": "learn-by-building",
"language": "JavaScript"
}
All rights reserved. Printed in the United States of America. This publication is protected
by copyright, and permission must be obtained from the author prior to any prohibited
reproduction, storage in a retrieval system, or transmission in any form or by any means,
electronic, mechanical, photocopying, recording, or likewise. For information regarding
permissions, contact:
Scripture quotations taken from the New American Standard Bible® (NASB).
Copyright © 1960, 1962, 1963, 1968, 1971, 1972, 1973, 1975, 1977, 1995 by The Lockman Foun-
dation. Used by permission. www.Lockman.org
iii
iv Functional Design Patterns for Express.js
Contents
Acknowledgments ix
Technical Reviewers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ix
Introduction xi
Why Express? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xi
Approach . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xii
Topics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xii
Prerequisites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xiii
Let’s Get Started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xiv
I Express Essentials 1
2 Responding to Requests 13
Simple Servers with the http Module . . . . . . . . . . . . . . . . . . . . . . . . . 14
Speaking HTTP over Telnet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Responding to Different Routes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Hello, Express . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Express Shorthands . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Go Further . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Multiple Response Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3 Express Router 23
Refactoring with the Router Pattern . . . . . . . . . . . . . . . . . . . . . . . . . 23
Express Router . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Functions with Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Routes with Dynamic Segments . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
v
vi Contents
II Middleware 53
5 Middleware 55
Cross Cutting with Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
Passing Data to Routes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
Route Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
Middleware is Everywhere . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
Go Further . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Error Handling Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
6 Common Middleware 67
Logging with Morgan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
Body Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Middleware Factories . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
Compression . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
Serving a Frontend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
File Uploads with Multer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Serving Static Files with a Path Prefix . . . . . . . . . . . . . . . . . . . . . . . . . 78
Accepting Multiple Body Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Go Further . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
URL Encoded Bodies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
PATCH Things Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
MIME Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
7 Basic Authentication 85
Authorization Header . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Handling Authentication with Middleware . . . . . . . . . . . . . . . . . . . . . . 87
Graceful Global Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
Requiring Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
Creating a Middleware Factory . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Currying and Middleware Factories . . . . . . . . . . . . . . . . . . . . . . . . . . 95
Contents vii
Go Further . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Hashing Passwords . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Index 127
viii Contents
Acknowledgments
A few months ago, writing a book wasn’t on the radar. Despite taking a travel sabbatical
to invest in photography, I was mentally, physically and emotionally exhausted. I flew
back to the States to figure out what was wrong.
I’ve always felt self sufficient, but for the first time in my adult life, I didn’t have a plan for
what was next. And that alone unnerved me — my goal-achieving nature needed to figure
the next thing out. But after a season of being completely drained, there wasn’t much to
lose if I went back to basics and acknowledged Jesus in my planning. Instead of asking
for His approval after I made my plans, I needed Him to produce the next step in me.
Jesus taught me what it means to live by faith: to so trust Him for tomorrow’s step, that
I stop making backup plans. To so rely on His promises, that I boldly ask Him to keep
them.
These have been some of the best months of my life. Unplanned, miraculous in the day-
to-day, outside my control and not on my bucket list. Everything I needed to get this
book done fell right into place in spite of me. So it seems fitting to acknowledge Him,
just as the composer Johann Sebastian Bach did:
Technical Reviewers
A technical book takes a special kind of reviewer, and I can’t thank my technical review-
ers enough — not just for their expertise, but for investing in me over the years. These
ix
x Acknowledgments
are the kindest and most brilliant developers I’ve had the privilege to call my friends and
coworkers.
Jay Hayes and I worked at Big Nerd Ranch for five years as consultants and instructors.
He has cheered me on and poured constant encouragement into me. His motivation,
kindness and glowing regard have been the energy behind many of my projects, and I
can safely attribute the reality of this book to his contagious curiosity.
Chris Aquino lined up my first opportunity to teach a web bootcamp six years ago. He
has an inexplicable ability to believe in people, and whatever the hierarchical relation-
ship — colleague, team manager, co-writer, co-instructor and friend — he has been be-
lieving in me, cheering me on and teaching me to be a better human. I wouldn’t be doing
what I love without his relentless positivity and kindness.
Joshua Martin has seen this book in many forms over the past year: bootcamps, video
content, bullet points… it would be more accurate to call him my technical “pre”reviewer.
He believes I can do anything, and in mentoring him as a developer, he has mentored me
in being a better human being.
Josh Justice is arguably the kindest, punniest curmudgeon on Twitter. It’s a rare poly-
glot who has an exceptional understanding of programming languages and the English
language. When we worked together at Big Nerd Ranch, he was probably the only other
developer with an opinion on serial commas and expertise in hyphenated words. Ex-
cellence, encouragement, razor-sharp feedback (look Josh, it’s hyphenated!) — by the
time Josh finished reviewing, I knew a stray “it’s” would be more miraculous than getting
tar -czvf right the first try.
Introduction
Learn the design patterns that transcend Express.js and recur throughout high-
quality production codebases.
You’ve built backends in another language for a decade. You’re a seasoned frontend
JavaScript developer. You’re a recent web bootcamp graduate. You’re searching for an
Express.js primer that isn’t another screencast or exhaustive reference guide.
This book is for you. The pedagogical approach of this book is aimed at transferring
design intuitions — motivated by real-world consulting experiences — in the fastest
way possible. That translates to a razor-focused topic scope and no contrived examples
to motivate tools you probably won’t use, or shouldn’t be using because they indicate
deeper “code smells.”
If you’re looking for an exhaustive Express reference guide, prefer to read passively, or
value books and video courses by their length, this book isn’t for you — unless you’re
looking for a handsome adornment for your bookshelf!
Why Express?
Express is arguably the ubiquitous library for building Node backends. It is partly re-
sponsible for Node’s surge in popularity, and many other Node frameworks build on top
of Express. As of mid-2019, it is a dependency of 3.75 million codebases on Github alone.
So if you hop into a Node codebase, chances are Express is part of it.
Express 5 is in development, but because a sizable group of tech giants depend on the
API — directly or through a dependency — Express has essentially been on feature freeze
for some time and is unlikely to see substantial overhauls.
This book steers away from version peculiarities and clever utility methods in favor of
good design patterns. Thanks to these patterns, the backend we will build together has
been rewritten in two other Node.js backend libraries with minimal changes.
xi
xii Introduction
Good design in an Express.js backend is good design anywhere. Some design patterns
may be more idiomatic in one language than another, but the patterns you learn to de-
velop Node backends will outlive Express and influence your design approaches in unre-
lated platforms.
Approach
There are countless books out there on backend design, so what makes this one differ-
ent? In a word, the approach.
Many well-meaning books and courses are built on a more-is-better ethos: a single step-
by-step course about Express is crammed with tangential topics like ES2015 JavaScript,
databases and React. When the teaching approach and learning outcomes become sec-
ondary to the topic list, the result is a grab bag of goodies that entertains the developer
rather than educates.
As a globetrotting educator, author and international speaker with a passion for craft,
I’ve guided hundreds of developers — from career switchers to senior developers at For-
tune 100 companies — through their journey into web development.
Both in the workplace and in the classroom, I’ve watched the entertainment model of
learning cripple developers. So over the last six years of teaching one to sixteen week
bootcamps, I’ve developed a pedagogical approach for developers at all skill levels.
Pedagogy — the method and practice of teaching — asks the essential question, what
does it mean to teach well? My approach to vocational teaching is based on a few axioms:
Like a well-designed app, good pedagogy becomes a transparent part of the learning
process by removing obstacles to learning — including itself!
Topics
This course focuses on best practice, conventional backend design for pure backend
APIs. It is not exhaustive, comprehensive or targeted at advanced Express developers
who are trying to scale huge legacy backends.
Prerequisites xiii
• ES2015–ES2017 JavaScript
• RESTful conventions
• Databases
• Node essentials
• Frontend
• Cookies and sessions
• Passport.js
• Templating
• Niche Express methods, especially if they are symptomatic of design flaws.
Instead, it is this book’s intention to equip developers — who already have a thorough
applied knowledge of JavaScript, some light Node experience, and who have preferably
built a backend before in any language or framework — with design insights.
Prerequisites
• You are immensely comfortable with async programming in JavaScript with call-
backs, async functions and Promises.
• You have ES2015 (previously called ES6) under your belt, especially destructuring
syntax and arrow functions.
• You have an experiential understanding of HTTP, though a rigorous understanding
is unnecessary.
Some things are not required to get the most out of this book! You don’t need prior back-
end experience. If you understand how servers and clients interact, experience from
either side of the equation is sufficient.
Throughout this book, we’ll be building a full-featured Express backend together called
Pony Express. Starting from an empty directory, we will intentionally bump into code-
base growing pains to motivate functional design patterns and Express features.
But first, in the next chapter we’ll detour from Node altogether and demystify the core
abstraction of the web: HTTP.
Part I
Express Essentials
1
Chapter 1
You’ve probably built backends or frontends before, but what exactly happens when you
load up www.google.com in the browser? When your React-powered frontend fires an
AJAX request to the backend API, how does the backend handle those requests?
If you dialed up a server without a browser, would you know what to say?
Many seasoned web developers — whether backend or frontend — don’t always have a
concrete idea of what goes on between the frontend and backend, and that’s okay! In
fact, it’s a testament to the web platform’s exceptional choice of abstractions.
It also means any developer — newcomer or senior — can level up their web chops by
dispelling the magic of Hypertext Transfer Protocol, better known as HTTP.
HTTP is the universal language of the web. That ubiquity means developers can produce
groundbreaking web experiences without knowing the nuances of HTTP. So is it worth
developing a fundamental understanding of HTTP?
HTTP is a great example of an abstraction for backend and frontend communication. Its
flexibility embodies the decentralized, democratic spirit of the web and allows clients
and backends with completely different capabilities to collaborate.
• They expose a self-consistent API that culls infinite possibilities to a few domain-
focused capabilities.
• The boundaries between API functions show cohesion, which means each func-
tion focuses on one responsibility.
• They cover up distracting differences in the underlying technology.
• They provide a common vocabulary, such as a Domain Specific Language (DSL).
• They are idiomatic to the intended programming language or platform.
• They tend to guide developers into good design patterns.
• They suggest idiomatic use cases and hint at underlying strengths and limitations.
3
4 Chapter 1. How Servers Talk
The last two characteristics are some of the most compelling reasons to become inti-
mately familiar with HTTP at a fundamental level: by stepping down to the level of an
HTTP conversation, it’s easy to determine which design approach will be idiomatic. And
when things go wrong, you will know how to dig for the problem.
Most of this chapter won’t be new to you, but just to be sure, let’s pull back the covers
on how frontends and backends communicate with HTTP. If you’ve worked on frontends
and backends for a while, HTTP may actually be simpler than you thought.
Installing telnet
Instead of using a browser to view a website, let’s drop down to the HTTP level and have
our own text conversation with http://expressjs.com using the telnet TCP client.
The telnet client comes with many Linux-based operating systems, so check if you
already have it with the which command:
$ which telnet
/usr/local/bin/telnet
The which command shows where a command is installed. It’s okay if a different path
is printed — such as /usr/bin/telnet — but if nothing is printed out, telnet is not
installed.
On Linux
Most Linux-based operating systems already include the telnet client, but if
which telnet doesn’t print anything, you can install it with the OS’s bundled package
manager:
# On Ubuntu:
$ apt-get install telnet
# On Fedora:
$ yum install telnet
On macOS
telnet is a common omission on macOS, but easy to install through the Homebrew
package manager. You’ve probably already installed Homebrew:
An HTTP Conversation with telnet 5
$ which brew
/usr/local/bin/brew
If nothing shows up, go to https://brew.sh and follow the famous one-line installation
instructions. Installing telnet — and many other developer staples from the Linux
world — is easy with Homebrew:
Restart your terminal and run which telnet again. If a path prints out, you’re all set!
Let’s start our own HTTP conversation using telnet . Type the following in your termi-
nal:
$ telnet expressjs.com 80
Like your browser, the telnet command is a client or user agent: it initiates the TCP
connection and makes the requests.
Clients Server
“User Agents” “Backend”
expressjs.com/
Browser
Express
Request
Telnet $ telnet
> GET/ expressjs.com
Response
GET /
Insomnia
To request the homepage like a browser would, we need to specify the path, which is
everything that comes after the domain, expressjs.com . The path for the homepage is
/.
The connection probably timed out while you were reading, so don’t forget to rerun the
telnet command. Next, type this request in your telnet session and hit the return
key:
GET / HTTP/1.1
1. The HTTP method — also called the HTTP verb — such as GET , POST or
DELETE .
2. The resource path, such as / , /homepage.html or /posts/1.json .
3. The HTTP version. If this is omitted, it is assumed to be the good ol’ 1991 version
of HTTP, HTTP/0.9 . When we test our own backend in the next chapter, you can
occasionally omit the HTTP version, but most servers only support HTTP/1.1 and
will return 400 Bad Request if omitted.
That’s enough for a valid request, but because the same IP address commonly hosts
many domains, we need to include one request header, Host , so the server knows for
certain which domain we are trying to connect to.
Assuming your telnet session hasn’t timed out, type this header immediately after the
request line you just typed in:
Host: expressjs.com
GET / HTTP/1.1
Host: expressjs.com
Hit the return key a couple times. The server waits for an empty line to indicate the re-
quest is finished, then it responds. Here’s an abridged example of what the response
might look like:
An HTTP Conversation with telnet 7
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Connection: keep-alive
Vary: Accept-Encoding
Cache-Control: max-age=600
<!DOCTYPE html>
<html lang="en">
<head>
<title>Express - Node.js web application framework</title>
[···]
1. The first line details the HTTP version and response status code. 200 OK means
the expressjs.com server understood our request.
2. The response headers come between the first line and empty line. This is prob-
ably where you’ve spent a good deal of time debugging your own backends and
frontends!
3. The response body is everything that comes after the blank line. Since we asked
for the homepage, the body is an HTML document.
That’s it! The complex behavior of visiting a website or making an AJAX request to a
backend API is actually a human-readable conversation that starts with one request and
ends with one response. This brief conversation is called the request-response cycle.
Request
expressjs.com/
Express expressjs.com
Response
Client Backend
HTTP/1.1 200 OK
Content-Type: text/html
<!DOCTYPE html>
<html lang="en">
...
The first version of HTTP (0.9) immediately closes the connection after one request-
response cycle. Later versions of HTTP keep the connection open by default for better
performance, so it feels more like chatting with a chatbot.
When you’re done talking to the server and reminiscing about the web’s old-fashioned
origins, tap Ctrl-C to close the connection.
8 Chapter 1. How Servers Talk
Talking to a backend API is no different from talking to a webpage server: the main dif-
ference is the contents of the response body. Websites like http://expressjs.com are
HTTP servers that respond with HTML documents as the body. Backend APIs are HTTP
servers that typically respond with JSON-formatted strings.
Our server will be a pure backend API, so it will reply with JSON-formatted strings. Let’s
get a feel for what a backend API looks like from telnet :
$ telnet jsonplaceholder.typicode.com 80
JSONPlaceholder is a fake JSON backend API with endpoints and conventions similar to
many backends. Let’s look up a post:
Don’t forget to hit return twice. The response should look like this:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 292
Connection: keep-alive
Vary: Origin, Accept-Encoding
Cache-Control: public, max-age=14400
{
"userId": 1,
"id": 1,
"title": "sunt aut facere...",
"body": "quia et suscipit\nsuscipit..."
}
As HTTP clients go, telnet and the browser are at either extreme: telnet is a bit te-
dious for testing a backend, while the browser is intended for rich web experiences. As
we build our backend, we will use the Insomnia HTTP client as a happy middle ground.
Insomnia is a purpose-built GUI for testing backend APIs. It saves requests as you cre-
ate them, so it’s dead simple to resend them as your backend evolves. Let’s revisit our
first HTTP request to the Express homepage. Click “New Request” and name it “Express
Homepage”, then change the method to “GET” and click “Create”.
Insomnia is split into two panels. The left panel is where we can customize anything
about the request: request headers, request body, HTTP method and the URL. Change
the URL to http://expressjs.com/ and click “Send”.
The right panel shows everything related to the server’s response. By default, it shows a
nice preview of the webpage. Select “Raw Data” from the “Preview” drop-down menu —
that’s exactly what we saw in our telnet session.
Figure 1.6: After changing the Preview to Raw Data, the response looks like it did in telnet.
The response headers under “Header” should also look the same as our telnet
session. The response body was HTML code, so the response also includes a
Content-Type: text/html header.
Making Requests with Insomnia 11
You can see the entire text conversation between Insomnia and http://expressjs.com/
from the “Timeline” tab:
Figure 1.8: No magic here, Insomnia is really just texting, telnet style.
12 Chapter 1. How Servers Talk
Let’s repeat that for a JSON backend API. Create a new Insomnia request called “View
Post”, set the request URL to http://jsonplaceholder.typicode.com/posts/1 and click
“Send”:
Figure 1.9: Backends usually respond with JSON and include Content-Type: applica-
tion/json.
The response from JSONPlaceholder is still text, but it’s formatted as JSON and includes
a Content-Type: application/json header. That’s pretty much the only difference
between a webpage server and a backend API: the response body and content type.
Go Further
If you want to continue experimenting with telnet and Insomnia, there is an expansive
compilation of public backend APIs at https://github.com/toddmotto/public-apis that
includes everything from weather APIs to bacon ipsum APIs.
In the next chapter, we’ll begin building our own backend API, Pony Express.
Chapter 2
Responding to Requests
The best way to motivate the design challenges of an Express backend is to build one!
We’ll be developing Pony Express, a simple mail server that stores emails and users. The
Pony Express backend API won’t speak SMTP or IMAP like a traditional mail server, but
will behave more like a conventional JSON backend API.
$ mkdir pony-express
$ cd pony-express
$ node --version
v10.15.3
Our Node project needs a package.json file. Make sure you’ve switched into the
project directory, then initialize a package.json file with default options:
$ npm init -y
Open your project folder in your preferred text editor. If you are using Visual Studio
Code, you can open an editor from the terminal with the code command:
13
14 Chapter 2. Responding to Requests
$ code ./
Let’s start off Pony Express with the built-in http module and respond to any request
by echoing the request method and URL. Create a new file, index.js :
index.js
$ node index.js
Keep in mind that we don’t have any console.log() statements, so you won’t see any-
thing printed in the terminal. So long as it hangs and doesn’t print out a stack trace, it’s
running!
When you open http://localhost:3000/emails in your browser, you should see “You
asked for GET /emails” printed. That’s it! We’ve built a server with Node’s http mod-
ule.
Together, the request method GET and request URL /emails are called a route. Al-
though production backends may respond to hundreds or thousands of unique routes,
Speaking HTTP over Telnet 15
all that logic lives in one callback: the argument to http.createServer() . That call-
back — called the request handler or request listener — is where the entire brains of
a backend server lives. When an HTTP request comes in, http.createServer() runs
this callback. So whether fifty browsers connect to http://localhost:3000/emails or
one browser opens fifty tabs to this URL, the request handler callback will execute fifty
times.
Notice that a request handler receives two arguments, both objects: req and res ,
which are short for “request” and “response”. The request object contains all the details
about the request the server received, so most of the time you should treat req as
read-only. Here’s an abridged example of what a request object looks like:
http.IncomingMessage
{
headers: { accept: "application/json" },
httpVersion: "1.1",
method: "GET",
trailers: {},
upgrade: false,
url: "/emails",
}
The response object is a bit more confusing: even though it is an argument to our
request handler, it doesn’t really have any information in it. Instead, it’s a blank box
for the request handler to fill — through mutation or the attached methods — and
http.createServer() will formulate the real HTTP response to the client that made
the request. So while req is read-only, res is write-only.
Because the names are so similar, it’s easy to mix them up and accidentally write to the
request object or read headers from the response object. If you run into strange errors
while following step-by-step, double check for req vs. res mixups!
Eventually Pony Express will receive requests from fancy clients like a frontend web app.
But to start, let’s talk to it with the telnet client so we can build a concrete mental
model. You’ll want to leave your server running, so open a new terminal window for your
telnet session and make a GET /emails request:
Don’t forget to tap the return key twice. The server response should look like this:
16 Chapter 2. Responding to Requests
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 19
Except for the first line, the server response is structured exactly like the request. The
res.end() method added a response body after the headers and automatically inserted
a Content-Length header.
Our backend isn’t very useful, how can we make it send different responses for different
routes? We can start with the simplest solution: add an if...else statement.
Pony Express is a mail server, so there should be routes to list users and emails. Just so
we don’t get bogged down in database stuff, download some sample emails and users as
JSON from learn.nybblr.com/express/fixtures.zip. Unzip the download and move the
entire fixtures/ directory into your project directory so it looks like this:
When a client makes a request for users or emails, let’s respond with the respective ar-
ray as JSON:
Responding to Different Routes 17
index.js
server.listen(3000);
Don’t forget to restart the server after you finish your changes.
From now on, we’ll make requests through Insomnia and only whip out telnet
occasionally to dispel magic. In Insomnia, create a new request for GET /users and
GET /emails , then try sending both.
Insomnia shows all the response details in a nice UI on the right, but you can always
see the raw text conversation by switching to the Timeline tab. Looks pretty similar to
telnet !
18 Chapter 2. Responding to Requests
Figure 2.3: Insomnia’s timeline tab shows the raw HTTP conversation.
Notice the response headers. It’s pretty empty right now, but since our server responds
with a JSON-formatted body, it’s supposed to include a Content-Type: application/json
header in the response. That doesn’t come for free, but we’ll fix it later.
Restarting our backend after every change is getting old, let’s install nodemon , a drop-in
replacement for the node command that automatically restarts the server when any-
thing in the directory changes:
What’s with npx ? The npx command allows us to run terminal commands that
come bundled with a Node module, but aren’t installed globally. If you happened to
install nodemon with npm install -g nodemon , you could drop the npx part, but
it’s better practice to bundle developer tools with the project by listing them in the
devDependencies section of package.json .
From now on, we’ll boot up the server using npx nodemon index.js .
Hello, Express
The entire brains of your backend lives in that callback function to http.createServer() ,
and it will become massive if we don’t break it up.
Express Shorthands 19
That’s where Express comes in: Express is a lightweight library for architecting the re-
quest handler callback. Let’s install Express:
index.js
server.listen(3000);
Save your changes, but don’t restart the server — remember, nodemon will do that auto-
matically. Test your Insomnia requests again to make sure everything works as before.
The express() function is a factory for building the request handler, but we still use
Node’s built-in http module to listen for incoming HTTP requests. Express is simply
in the business of helping you build a massive callback. That’s a significant contribution
since that’s where the entire brains of the backend lives.
Express Shorthands
That probably isn’t the syntax you’ve seen for booting up an Express backend. Because
Express apps are always used with Node’s http module, a popular shorthand is pro-
vided to boot up an Express app:
20 Chapter 2. Responding to Requests
index.js
[···]
- server.listen(3000);
+ app.listen(3000);
The app.listen() method does exactly what we had before. Most developers stick
with this shorthand, but it helps to break it out into what’s actually going on. It also
demonstrates one reason Express is so popular: it doesn’t replace or even augment the
http module, it just helps you build the request handler.
However, Express does add some useful methods to the response object. Let’s use
res.send() to automatically convert the list of emails and users into JSON-formatted
strings:
index.js
[···]
[···]
Check the route with Insomnia: the response headers have a few additions!
Go Further 21
It added a Content-Type header! Express helper methods do a lot of nice things like
that. While the vanilla .end() method is Node’s generic stream method and just sends
strings as is, the .send() method typically responds with JSON and adds response
headers such as application/json . Since Pony Express will mostly respond with
JSON-formatted bodies, we’ll almost always use .send() in place of .end() .
The Pony Express is off to humble beginnings. We aren’t really leveraging any of the fea-
tures Express offers, but perhaps Express’s best feature is that it doesn’t replace the
http module: in fact, it builds on it. That alone makes Express an exceptional founda-
tion for building complex backends, because as the requirements expand we’ll be limited
by Node’s fantastically robust http module, and not by Express!
Go Further
There are many nifty features and tips we don’t have time to cover throughout the book
— that’s where you come in! Some chapters include challenges for you, the reader, to
try on your own. It’s easy to turn into a zombie typist in a step-by-step guide, so these
exercises are an invitation to leave your undead form behind and level up!
Right now, the GET /users and GET /emails routes always respond with JSON, but
clients can specify a preferred response Content-Type by including an Accept header
in the request:
22 Chapter 2. Responding to Requests
In addition to JSON, add support for a CSV or XML response based on the Accept re-
quest header. Insomnia sets the Accept header to */* by default, but you can override
that by adding an Accept header to the request:
If the Accept header is not specified, default to a JSON response. A few tips on this
challenge:
• Don’t manually parse the Accept header — it’s fairly complex. Instead, check out
Express’s req.accepts() method. Also, don’t manually set the Content-Type
header on the response; instead, use Express’s res.type() method.
• There are plenty of modules to generate CSV. Check out the stringify()
method of the aptly named csv node module.
• To generate XML, check out the xmlbuilder module.
Chapter 3
Express Router
Backend APIs often respond to hundreds or thousands of unique method and path
combinations. Each method and path combination — such as GET /users or
POST /emails — is called a route. But no matter how many routes your backend API
supports, every single request will need to be processed by a single request handler
function. That means index.js will grow with every new route: even if each route took
only one line of code, that’s a large file and a nightmarish recipe for merge conflicts.
How can we architect the request handler callback such that, for every new route, the
number of files grows while the average file length stays the same? Put another way,
how do we design a backend so the codebase scales horizontally instead of vertically?
The easiest way to accomplish this is by applying the Router design pattern, not to be
confused with Express’s Router API. The Router design pattern is a common refactor to
obliterate ballooning switch statements or if...else statements that share similar
predicates.
One of the strengths of this refactor is that, at each step in the refactor, the code should
still run so you can catch bugs early on. Try not to skip ahead, but take the refactor one
step at a time.
In the request handler of index.js , extract the body of each case into a function:
23
24 Chapter 3. Express Router
index.js
[···]
The second step is to replace the body of each case with its function. If your functions
were invoked with slightly different arguments, you’d need to do a little extra refactoring.
Since both routes have the same function signature, we can continue with the refactor:
index.js
[···]
[···]
Our code should still work after each step in the refactor, so give your GET /users and
GET /emails routes a quick test with Insomnia.
Refactoring with the Router Pattern 25
The third step is to create some sort of mapping from the predicate condition to a corre-
sponding route. Since the if...else conditions are always a comparison with a string
like "GET /emails" , we can use a plain ol’ JavaScript object:
index.js
[···]
+ let routes = {
+ 'GET /users': getUsersRoute,
+ 'GET /emails': getEmailsRoute,
+ };
The fourth and final step is to replace the if...else cases with a single lookup in the
list of routes:
26 Chapter 3. Express Router
index.js
[···]
[···]
What about that last else statement? We still need a fallback to catch any unknown
routes like GET /spam , but you could extract the logic into a separate function like
noRouteFound() to remove the if...else statement altogether:
Express Router 27
index.js
[···]
- if (handler) {
handler(req, res);
- } else {
- res.end('You asked for ' + route);
- }
});
[···]
Send a few requests with Insomnia to make sure the routes still work. Huzzah! We elim-
inated a growing if...else statement, and in the process extracted individual routes
outside the request handler.
Express Router
Now that we’ve applied the Router design pattern, which part is the “router”? In this
context, a Router is a function whose only responsibility is to delegate logic to another
function. So the entire callback to app.use() is a Router function!
Let’s make this a bit more obvious by assigning the request handler callback to a variable
before passing it to app.use() :
28 Chapter 3. Express Router
index.js
[···]
handler(req, res);
- });
+ };
+ app.use(router);
app.listen(3000);
While route functions are unique to the particular backend you’re building, the router
function we extracted is common to all backends. Well, that just happens to be Express’s
flagship feature: express.Router() generates a Router function much like the one we
just wrote. Let’s swap it in!
index.js
[···]
- let routes = {
- 'GET /users': getUsersRoute,
- 'GET /emails': getEmailsRoute,
- };
+ router.get('/users', getUsersRoute);
+ router.get('/emails', getEmailsRoute);
app.use(router);
[···]
Whoa, look at that! This behaves identically to what we had before, but it’s much terser.
Express’s Router provides an expressive API for creating a route map with methods like
router.get() .
Functions with Methods 29
Test out your routes with Insomnia once more — despite the mass deletions, the back-
end should respond identically to before.
But wait, the router() generated by express.Router() is a function, just like the
handwritten router() it replaces. If router() is a function, why does it have meth-
ods like router.get() ?
This is a recurring API design style for JavaScript libraries, and especially Express. In
fact, we already saw that express() returns a function we called app() , yet app has a
.use() method. Here’s a shortened example:
server.listen(3000);
In JavaScript, functions are also objects: that means they can be invoked, but also have
methods. Unsurprisingly, JavaScript functions are called function objects. In Express,
this duality makes it easy to seamlessly combine vanilla functions with libraries that in-
clude an elegant configuration API.
Our server is made of many small functions, so it should be trivial to tease apart the
codebase as it grows. But before we test that theory out, let’s add a couple more routes.
30 Chapter 3. Express Router
If an API client needs to look up a particular user, the conventional route would be
GET /users/<id> . For example, GET /users/1 should respond with the JSON-
formatted data for the user with ID #1. Express’s Router makes it easy to match
wildcards like this with Dynamic Segments:
index.js
[···]
[···]
router.get('/users', getUsersRoute);
+ router.get('/users/:id', getUserRoute);
[···]
Dynamic segments are denoted by a colon and short name, such as :id . When a re-
quest for GET /users/1 comes in, it will match the route for /users/:id , and the
dynamic segment :id will match 1 . The getUserRoute() function can retrieve that
value in req.params.id .
Create a new request in Insomnia for GET /users/1 , then fire it off. The request will
hang, but in the terminal you should see an object with one key-value pair logged:
{ id: '1' }
A route can be defined with more than one dynamic segment. For example, to add a
route that lists all emails from user #1 to user #2, you could add a route like this:
index.js
[···]
[···]
Try out GET /users/1 in Insomnia. This time, the server should respond with a JSON
object for just user #1. Let’s add a similar route for looking up an email by ID:
index.js
[···]
[···]
router.get('/emails', getEmailsRoute);
+ router.get('/emails/:id', getEmailRoute);
app.use(router);
[···]
Keep in mind that, since the entire path is just a string, dynamic segments in
req.params will always be strings too, even if the value looks like a number. If
your IDs are stored as integers in your database, the type mismatch can cause some
irritating bugs.
Create a new Insomnia request for GET /emails/1 and test it out. It should behave
pretty much the same as the getUserRoute() and respond with just email #1.
32 Chapter 3. Express Router
You don’t need to have just one router. In fact, it’s a good idea to create a dedicated
router for each type of resource. That means we should have a router for all /users
routes and another router for all /emails routes:
index.js
[···]
- router.get('/users', getUsersRoute);
+ usersRouter.get('/users', getUsersRoute);
- router.get('/users/:id', getUserRoute);
+ usersRouter.get('/users/:id', getUserRoute);
- router.get('/emails', getEmailsRoute);
+ emailsRouter.get('/emails', getEmailsRoute);
- router.get('/emails/:id', getEmailRoute);
+ emailsRouter.get('/emails/:id', getEmailRoute);
- app.use(router);
+ app.use(usersRouter);
+ app.use(emailsRouter);
[···]
Why not leave all the routes on one router? As the backend’s functionality grows, the
codebase will tend to scale vertically — longer files on average — instead of horizontally.
By splitting each resource into its own router, the average file size will stay the same as
backend functionality grows; only the number of files will increase. Software that grows
like this is generally easier to maintain. It means each file focuses on one responsibil-
ity, and it becomes easier for several developers to work in the same codebase without
merge conflicts. It’s like a router for your routers!
When adding a router, we can specify a path prefix to app.use() that will be shared by
all its routes. Let’s try it in index.js :
Extracting Routers into Files 33
index.js
[···]
- usersRouter.get('/users', getUsersRoute);
+ usersRouter.get('/', getUsersRoute);
- usersRouter.get('/users/:id', getUserRoute);
+ usersRouter.get('/:id', getUserRoute);
- emailsRouter.get('/emails', getEmailsRoute);
+ emailsRouter.get('/', getEmailsRoute);
- emailsRouter.get('/emails/:id', getEmailRoute);
+ emailsRouter.get('/:id', getEmailRoute);
- app.use(usersRouter);
+ app.use('/users', usersRouter);
- app.use(emailsRouter);
+ app.use('/emails', emailsRouter);
[···]
Now that we have decoupled the routes of /users and /emails , it’s a cinch to extract
the usersRouter() and emailsRouter() into separate files.
Create a new folder called routes/ in your project directory, then create two files:
routes/users.js and routes/emails.js . Move the usersRouter() and its
corresponding route functions into routes/users.js :
34 Chapter 3. Express Router
routes/users.js
Don’t forget to require() Express and export the usersRouter() function. Let’s do
the same and move emailsRouter() with its route functions into routes/emails.js :
routes/emails.js
Now we can delete the migrated code from index.js and require() the two router
files:
index.js
[···]
[···]
That’s a lot of deletions, so make sure to test all your routes in Insomnia. By now,
index.js file should be nice and short:
36 Chapter 3. Express Router
index.js
app.use('/users', usersRouter);
app.use('/emails', emailsRouter);
app.listen(3000);
1. We applied the Router design pattern to extract individual routes into functions
and remove the growing if...else statement.
2. We replaced our handwritten router() with express.Router() without modi-
fying any route functions.
3. We added routes to get a user or email by ID using dynamic segments.
4. We split routes for /users and /emails into two routers that live in separate
files.
As our backend continues to grow, index.js won’t grow that much. Instead, the num-
ber of files will grow as we add backend functionality. Moreover, very little of our exist-
ing code will be modified: we will mostly add new code.
Go Further
A few tips:
• As you apply the Router design pattern, the main difference is how data gets
converted to a string. Those differences can be extracted to an object called
formatters that maps from content types — json , csv , xml — to a function
that converts data to a string. The formatters object will closely resemble the
routes object we created earlier.
38 Chapter 3. Express Router
Chapter 4
Pony Express can retrieve existing emails, but clients also need a backend route to send
new emails. The conventional way to send details about a new resource — like an email
— is to include a request body.
In the same way the server’s response can include a body, a client’s request can include
a body. But what should the request body look like for creating a new email? Well, the
request body will look identical to the response body from getEmailRoute() : a JSON-
formatted string.
The RESTful convention for creating a new email would be a request to POST /emails
that includes a JSON-formatted request body. If we were to handwrite the HTTP re-
quest, it would look like this:
{
"from": "1",
"to": "2",
"subject": "Hello",
"body": "World"
}
39
40 Chapter 4. Working with Request Bodies
routes/emails.js
[···]
emailsRouter.get('/', getEmailsRoute);
emailsRouter.get('/:id', getEmailRoute);
+ emailsRouter.post('/', createEmailRoute);
[···]
How does createEmailRoute() access the request body? Well, since Express builds
on Node’s built-in http module, the req object is a stream object that represents the
request body, so we can listen for the data and end events:
[···]
[···]
Now to test it out! To build a solid mental model of how request bodies work with Node’s
http module, open a telnet session in a new terminal window:
We’ll start with a short JSON request body. Try entering this HTTP request one line at a
time, and keep an eye on the backend’s terminal as you do:
Request Body Lifecycle 41
{
"my": "body"
}
Your telnet session will probably blow up after you type the first line of the body:
{
HTTP/1.1 400 Bad Request
What just happened? Although it looks like you’re sending a request to the server all at
once, the HTTP protocol allows you to send a chunk at a time. Every time you hit the
return key, telnet sends the entire line to the backend. If you were watching the back-
end’s terminal, the logs indicate that createEmailRoute() started handling the request
as soon as you hit return twice in telnet .
However, Express wasn’t expecting to receive a body at all. Requests must include a
Content-Length header to let the server know a body will follow and how long the
body will be. If you try to include a body with a request but forget the Content-Length
header, Express will automatically respond with 400 Bad Request .
Let’s retry the request, but this time include a Content-Length header to specify the
body’s length in bytes. It might help to compose your HTTP request in a text editor first
for tweaking, then copy-paste the entire request into telnet . Depending on your op-
erating system and text editor, line breaks count as 1 or 2 bytes. In telnet , line breaks
count as 2 bytes for a total byte length of 20:
42 Chapter 4. Working with Request Bodies
{
"my": "body"
}
When you finish typing the request body in telnet and hit return, nothing will happen.
However, there should be a few log statements in the backend terminal:
Creating email...
Chunk...
Chunk...
Chunk...
Received body...
That’s good! It means Express received our request body without complaints, but
createEmailRoute() isn’t doing anything with the body yet.
The request body is streamed over in chunks, but the createEmailRoute() function
needs the entire JSON-formatted request body. Let’s write a helper function to buffer up
the request body into one string. Make a new directory called lib/ , then create a file
called lib/read-body.js :
lib/read-body.js
The readBody() function collects each “chunk” of the body it receives and returns a
Promise that resolves once the entire request body has been received. When sending an
HTTP request with telnet , each chunk will be one line of the body. In Node, chunks
are Buffer objects — Node’s binary datatype — but can easily be converted to a string
for debugging.
routes/emails.js
[···]
+ console.log('Received body...');
+ console.log(body.toString());
};
[···]
Hop back into telnet to retry your POST /emails request and keep an eye on
the server logs in the other terminal window. As you type each line of the body,
readBody() should immediately print each line:
44 Chapter 4. Working with Request Bodies
Creating email...
Chunk: {
Chunk: "my": "body"
Chunk: }
Received body...
{
"my": "body"
}
Now let’s use the request body to add a new email to the emails array:
Finishing Up the Create Endpoint 45
routes/emails.js
[···]
[···]
Rerun your POST /emails request in telnet , but this time specify a full email in the
request body. It may take a few tries to get the request just right, so you may want to
compose the entire HTTP request in a text editor, then copy-paste it into telnet :
{
"from": "1",
"to": "2",
"subject": "Hello",
"body": "World"
}
To make sure it worked, hop into Insomnia and check the response for GET /emails .
The response should include a new email!
We have a couple loose ends to wrap up. First, the server doesn’t respond to the request
after it’s finished creating the email. The conventional response after creating a new re-
source is a 201 Created status code and a JSON-formatted body:
46 Chapter 4. Working with Request Bodies
routes/emails.js
[···]
[···]
Rerun the HTTP request in telnet . Once you finish typing the body, the server should
respond with exactly the same body:
{"from":"1","to":"2","subject":"Hello",
"body":"World"}
One last thing: every email needs an id attribute so we can look it up later. We
aren’t using a database which would generate the ID automatically, but we can
write a quick helper function to generate a random ID. Let’s put it in a new file,
lib/generate-id.js :
lib/generate-id.js
routes/emails.js
[···]
[···]
Try the request once more in telnet . The response should include an "id" attribute.
{"from":"1","to":"2","subject":"Hello",
"body":"World","id":"df830371a64cab4e"}
We made these requests with telnet to help dispel the magic around request bodies,
but from now on we’ll stick to Insomnia.
Let’s mirror what we’ve done in telnet with a new Insomnia request. First, create a
new Insomnia request for POST /emails — don’t forget to change the method from
GET to POST . Then, to include a JSON-formatted body, select the “Body” tab and
change the type from “No Body” to “JSON”.
48 Chapter 4. Working with Request Bodies
After changing the body type, paste in the JSON-formatted email contents you
were using in telnet . Insomnia will take the initiative and automatically include a
Content-Length and Content-Type header, so don’t add your own.
Try sending the Insomnia request. The response should look just like it did in telnet .
Now that Pony Express supports creating and viewing emails, let’s finish up the CRUD
(Create-Read-Update-Delete) functionality with routes to update or delete an existing
email in routes/emails.js :
Update and Delete 49
routes/emails.js
[···]
emailsRouter.get('/', getEmailsRoute);
emailsRouter.get('/:id', getEmailRoute);
emailsRouter.post('/', createEmailRoute);
+ emailsRouter.patch('/:id', updateEmailRoute);
[···]
In Insomnia, make a new request to update the subject of an existing email, such as
PATCH /emails/1 . The request body should be set to JSON and look like:
{
"subject": "I've been changed!"
}
After sending the request, follow up with a request to GET /emails/1 to confirm that
the subject changed. Keep in mind that these changes aren’t backed by a database, so
every time the backend restarts, the fixture data — emails and users — will be reset.
Since nodemon restarts every time the code changes, those updates will disappear fre-
quently.
There’s just one more route to go! Add a route for deleting an email:
50 Chapter 4. Working with Request Bodies
routes/emails.js
[···]
[···]
emailsRouter.patch('/:id', updateEmailRoute);
+ emailsRouter.delete('/:id', deleteEmailRoute);
[···]
The deleteEmailRoute() function looks up an email by ID, then mutates the emails
array to remove it. Again, mutation is generally bad practice, but is used for simplicity.
When deleting a resource, it’s conventional to respond with the 204 No Content status
code and omit the response body. Express includes a shorthand method to simultane-
ously set the status code and end the response, res.sendStatus() .
Create a new request in Insomnia for deleting an email, such as DELETE /emails/1 . To
verify that the email was successfully deleted, follow up with a GET /emails request
and ensure email #1 is no longer listed.
CRUD endpoints like /emails usually have a total of five routes. Because the route
path is either "/" or "/:id" , some developers prefer to use the Express Router’s
.route() method to cut down the repetition:
Go Further 51
routes/emails.js
[···]
- emailsRouter.get('/', getEmailsRoute);
- emailsRouter.get('/:id', getEmailRoute);
- emailsRouter.post('/', createEmailRoute);
- emailsRouter.patch('/:id', updateEmailRoute);
- emailsRouter.delete('/:id', deleteEmailRoute);
+ emailsRouter.route('/')
+ .get(getEmailsRoute)
+ .post(createEmailRoute)
+ ;
+
+ emailsRouter.route('/:id')
+ .get(getEmailRoute)
+ .patch(updateEmailRoute)
+ .delete(deleteEmailRoute)
+ ;
[···]
The .route() method accepts the path shared between routes — such as .get() and
.post() — and provides chaining syntax. This syntax is completely optional: in fact, it
is often preferable to stick with the prior format because it keeps version control com-
mits tidier when adding or removing routes. The .route() method can occasionally
make decoupling routes unnecessarily noisy.
Go Further
That was a long chapter, so you deserve a break. There are no challenges, except to cele-
brate completing the first module!
52 Chapter 4. Working with Request Bodies
Part II
Middleware
53
Chapter 5
Middleware
Often the same behavior needs to be added to a group of routes. For example, most
backends log every incoming request to the terminal for debugging and production au-
dits. How could we add logging to Pony Express?
Right now, it’s simple enough: we just prepend console.log() to each route function.
For example, we could start in routes/emails.js :
routes/emails.js
[···]
[···]
55
56 Chapter 5. Middleware
Express provides a method — app.use() — to insert a function that runs before any
routes below it. Let’s try it in index.js :
index.js
[···]
+ app.use(logger);
app.use('/users', usersRouter);
app.use('/emails', emailsRouter);
[···]
Notice the signature of the logger() function. Like a route function, it receives a re-
quest and response object, but it also receives a third argument called next . Any func-
tion with this signature is called middleware.
When a request comes in, the logger() middleware function runs before any of the
routers added below it with app.use() . These functions are called middleware because
they are sandwiched between each other, and the collective sandwich of these middle-
ware functions is called the middleware stack.
Cross Cutting with Middleware 57
Request Response
app(req, res)
Middleware
usersRouter( req, res, next ) app.use(usersRouter);
Stack
Figure 5.1: Each middleware function in the stack gets to run before those below it.
You may not have realized it, but there were already a couple layers in your middleware
stack: usersRouter() and emailsRouter() are middleware functions! Every instance
of app.use() adds a new layer to the bottom of the stack.
Hop into Insomnia and try a few requests like GET /users and GET /emails . In the
terminal, the backend now prints out the request method and path for any route! How-
ever, Insomnia seems to be hanging:
What’s going on? Middleware functions have a lot of power: not only can they be
inserted before routes, but they can decide whether to continue to the routes or skip
them altogether! To continue to the routes — the next layer in our middleware stack —
the middleware must invoke the third argument it received, next() :
58 Chapter 5. Middleware
index.js
[···]
[···]
Try a few requests with Insomnia. The backend should still log each request, but now
the routes should behave as they did before the hang.
Way to go, you wrote your first middleware function! Middleware is both a general de-
sign pattern and a concrete feature in backend libraries like Express. Express Middle-
ware helps us reuse complex behaviors — sometimes called cross cutting concerns —
across routes. Since middleware tends to be entirely decoupled from the routes, it’s in-
credibly easy to reuse middleware in other projects. In fact, let’s move logger() into a
new file called lib/logger.js :
lib/logger.js
index.js
[···]
[···]
Passing Data to Routes 59
lib/json-body-parser.js
Unlike our logger() middleware, jsonBodyParser() does some work that needs to
be passed to the routes. How should we feed the parsed JSON to the routes in the next
layer? In Express, it’s common to add a property to the request object. We’ll put the
parsed JSON body in req.body :
lib/json-body-parser.js
[···]
[···]
Now any route that comes after jsonBodyParser() can access the JSON-formatted
request body in req.body . Where should jsonBodyParser() go in the middleware
stack? We could try adding it to index.js like this:
60 Chapter 5. Middleware
index.js
[···]
app.use(logger);
+ app.use(jsonBodyParser);
app.use('/users', usersRouter);
app.use('/emails', emailsRouter);
[···]
Since all the /users and /emails routes come after jsonBodyParser() runs, we can
drop the readBody() calls from createEmailRoute() and updateEmailRoute() in
routes/emails.js :
Route Middleware 61
routes/emails.js
[···]
[···]
Retry your Insomnia requests for POST /emails and PATCH /emails/1 . They should
work just as before!
Route Middleware
Sadly not all is well. Try sending a GET /emails request with Insomnia. The request
seems to be hanging again because an exception is blowing everything up:
GET /emails
(node:44439) UnhandledPromiseRejectionWarning:
SyntaxError: Unexpected end of JSON input
at JSON.parse (<anonymous>)
at jsonBodyParser (lib/json-body-parser.js:5:19)
[···]
62 Chapter 5. Middleware
What’s going on? Well, not every route expects a request body, much less a JSON-
formatted body. But jsonBodyParser() runs before every single route as though
a JSON-formatted request body is guaranteed. GET requests don’t have a body, so
JSON.parse() is trying to parse an empty string.
There are a few approaches to fix this bug. The typical solution is to make jsonBodyParser()
a bit more robust to edge cases with some if...else statements. However, apart
from making our code uglier, it only postpones other bugs that will emerge because
it won’t solve the underlying design problem: only two routes in our backend expect
JSON-formatted request bodies!
Inserting middleware with app.use() is a bit like using global variables: tempting and
easy, but deadly to reusable software. With few exceptions, “global middleware” is a bad
design choice because it is more difficult to “opt-out” of middleware in a few routes than
it is to “opt-in” where it’s needed.
Request Response
app(req, res)
POST /emails jsonBodyParser( req, res, next ) createEmailRoute( req, res, next )
Route
Middleware
Figure 5.3: Route middleware is like a personalized stack for just this route.
Instead of adding global middleware with app.use() , we can specify middleware for
individual routes with .get() and its siblings. Let’s try it in routes/emails.js :
Route Middleware 63
routes/emails.js
[···]
emailsRouter.route('/')
.get(getEmailsRoute)
- .post(createEmailRoute)
+ .post(jsonBodyParser, createEmailRoute)
;
emailsRouter.route('/:id')
.get(getEmailRoute)
- .patch(updateEmailRoute)
+ .patch(jsonBodyParser, updateEmailRoute)
.delete(deleteEmailRoute)
;
[···]
You can add as many middleware functions for a route as you want. They will execute
from left to right, so make sure your route function comes last. To get our code working
again, let’s nuke jsonBodyParser() from the global middleware stack in index.js :
index.js
[···]
app.use(logger);
- app.use(jsonBodyParser);
app.use('/users', usersRouter);
app.use('/emails', emailsRouter);
[···]
Try a few requests again in Insomnia, like POST /emails , PATCH /emails/1 and
GET /emails . They should all be working as before!
64 Chapter 5. Middleware
Middleware is Everywhere
Middleware is a fundamental design pattern that emerges in codebases other than the
backend. But particularly in a backend, middleware allows us to reuse complex behav-
iors across many routes. Because we don’t have to worry too much about interactions
between middleware, the middleware pattern tends to insulate existing code from
change — a powerful characteristic for preventing regressions.
It is generally better to add middleware to just the route or group of routes that needs
the middleware’s functionality. That’s because it is always easier for a route to opt-in
to middleware than it is to opt-out of global middleware. There are a few compelling
exceptions which we’ll cover in upcoming chapters.
if (handler) {
handler(req, res);
} else {
next();
}
};
app.use(router);
That’s not all: route functions like createEmailRoute() are also middleware! Although
they only list two parameters — (req, res) => — they also receive next like any
other middleware function.
In JavaScript, the number of arguments used to invoke a function doesn’t need to match
the number of parameters listed in the function definition. Route functions are generally
the end of the line for an HTTP request, so they rarely need the last next argument.
Go Further
Pony Express is happy — that is, it only deals with the happy paths. But not all paths
are happy: what happens if a client requests an email that doesn’t exist, such as
GET /emails/4 ?
At the moment, it responds with 200 OK and an empty body. That response is uncon-
ventional and likely to frustrate frontend developers. Instead, the backend should re-
spond the same way it does to any path that doesn’t exist: with 404 Not Found .
This response logic tends to get duplicated across many routes, but there are a few ways
to DRY it up. One way is to throw a custom error from the route:
routes/emails.js
[···]
[···]
Exceptions have some unique design advantages: because they immediately interrupt
the route and all middleware below it, they help keep if...else statements to a mini-
mum. Moreover, exceptions naturally “bubble up” until caught, so it’s straightforward to
handle exceptions in one place.
Throw a NotFound error from all the /users and /emails routes that could poten-
tially look up a non-existent resource, then add one error handling middleware that
catches NotFound errors and responds with a 404 Not Found status code and body
of your choosing.
Chapter 6
Common Middleware
Middleware is pretty awesome. It’s such a good design pattern for code reuse that the
most common backend behaviors are already coded up and can be added with just a few
lines of code.
We will use very few third-party libraries in Pony Express, but these particular packages
are staples of the Express community. Most of the middleware in this chapter used to
be bundled with Express, but over time they have been extracted to separate packages.
Thus, unlike many libraries, the Express core has shrunk in newer releases.
Let’s start by fancying up our request logger. The Express team maintains logging mid-
dleware called Morgan that will easily replace our own logger:
We could delete lib/logger.js , but to show how easy it is to swap in third-party mid-
dleware without influencing dependent files, let’s modify lib/logger.js :
lib/logger.js
module.exports = logger;
67
68 Chapter 6. Common Middleware
Send a few requests with Insomnia. There should be some even fancier log statements in
the terminal!
It’s easy to swap in a middleware upgrade because all middleware has the same signa-
ture: (req, res, next) => .
By the way, is it bad that logger() is part of the global middleware stack? For most
backends, every single request should be logged with a consistent format. Consequently,
logging is a canonical example of when to add middleware globally.
Body Parser
What other handwritten middleware could we obliterate and replace with a third-party
package? Well, we wrote a fair amount of code for parsing JSON-formatted request bod-
ies, and we can drop in Express’s own body-parser package:
The body-parser package includes middleware for parsing several body formats, in-
cluding JSON:
Middleware Factories 69
routes/emails.js
emailsRouter.route('/')
.get(getEmailsRoute)
- .post(jsonBodyParser, createEmailRoute)
+ .post(bodyParser.json(), createEmailRoute)
;
emailsRouter.route('/:id')
.get(getEmailRoute)
- .patch(jsonBodyParser, updateEmailRoute)
+ .patch(bodyParser.json(), updateEmailRoute)
.delete(deleteEmailRoute)
;
[···]
Test out your POST /emails and PATCH /emails/1 requests in Insomnia. They
should work exactly as before! Unless you’re feeling sentimental, go ahead and delete
lib/read-body.js and lib/json-body-parser.js .
Middleware Factories
What’s up with the syntax for bodyParser.json() ? It’s extremely common to configure
a middleware function before inserting it. For example, we could prevent a client from
sending a huge JSON request body:
You might recognize .json() as an example of the Factory design pattern. Specifically,
bodyParser.json() is a Middleware Factory function.
This isn’t the first example we’ve seen: morgan('tiny') is also a Middleware Factory,
and we passed it an argument — 'tiny' — to configure the logging format. Middleware
Factories make middleware easy to customize without forcing all instances of that par-
ticular middleware to share the same configuration.
JavaScript excels in this style of functional programming, so we’ll try writing our own
Middleware Factory in an upcoming chapter.
Compression
We’re not done with easy wins yet! A lot of the HTTP traffic between a client and back-
end is unnecessarily bulky, but we can add compression support — both for the request
body and the response body — with the compression package:
It takes just a couple lines in index.js to add compression support to the entire back-
end:
index.js
[···]
app.use(logger);
+ app.use(compress());
app.use('/users', usersRouter);
app.use('/emails', emailsRouter);
[···]
Just like that, our backend can decompress requests and compress responses — quick
performance wins for the win! Like logging, compression is another good example of
when to use global middleware.
Compression 71
To test out compression, add the following request header to your GET /emails re-
quest in Insomnia:
Accept-Encoding: gzip
This tells the backend that Insomnia understands gzipped responses and to compress
the response if the backend also supports gzip.
Try sending the request. To verify that Express compressed the response, check the
Headers tab in the response section. When a server sends a compressed response, it
should include a Content-Encoding: gzip header:
Hmm, the Accept-Encoding header tells the backend to compress the response, so
why doesn’t the response have a Content-Encoding header? By default, the compres-
sion middleware only compresses response bodies when the size is beyond a threshold
like 1 kb. It’s a good default, but just to make sure we wired everything up correctly, let’s
temporarily disable the threshold for compress() :
index.js
[···]
app.use(logger);
- app.use(compress());
+ app.use(compress({ threshold: 0 }));
[···]
Retry the GET /emails request in Insomnia. This time, the response headers should
include a Content-Encoding: gzip header.
72 Chapter 6. Common Middleware
Compression is working! 1 kb is still a good default, so let’s comment out the threshold:
index.js
[···]
app.use(logger);
- app.use(compress({ threshold: 0 }));
+ app.use(compress(/* { threshold: 0 } */));
[···]
Serving a Frontend
Right now our backend is a pure API, but it would be nice to show a basic webpage when
a user opens http://localhost:3000 in their browser. In your project folder, make a di-
rectory for frontend files called public/ , then create a file called public/index.html :
Serving a Frontend 73
public/index.html
+ <!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>Pony Express</title>
+ </head>
+ <body>
+ <h1>Pony Express</h1>
+ </body>
+ </html>
We could add individual routes to respond to requests like GET /index.html , but as
the frontend grows to include CSS and images, we will need to add more routes. Instead,
we could write a single middleware function that examines all incoming requests and
maps the URL to a corresponding file in the public directory.
index.js
[···]
app.use(logger);
app.use(compress(/* { threshold: 0 } */));
+ app.use(serveStatic('./public'));
app.use('/users', usersRouter);
app.use('/emails', emailsRouter);
[···]
There’s one caveat: the path ./public is a relative path, but it’s not relative to the
project directory — it’s relative to the terminal’s present working directory. In produc-
tion environments, it’s common to boot up a Node server from somewhere other than
the project directory, so let’s calculate the full path to ./public based on the project
directory:
index.js
Our users may want to include file attachments when creating their emails. Unfortu-
nately, a JSON-formatted request body can’t include file uploads. However, there is an-
other well-supported body format that does: multipart form data. Like a simple JSON
object, multipart form data can represent a dictionary of keys and values: the keys are
strings, and the values can be a string or a file.
Create a new directory called uploads/ in your project. This is where email attach-
ments will be stored after upload. Since body-parser doesn’t support multipart form
data, most developers turn to the multer package:
File Uploads with Multer 75
routes/emails.js
[···]
emailsRouter.route('/')
.get(getEmailsRoute)
- .post(bodyParser.json(), createEmailRoute)
+ .post(
+ bodyParser.json(),
+ upload.array('attachments'),
+ createEmailRoute
+ )
;
[···]
As mentioned last chapter, a route can have multiple middleware functions that run
before it. The createEmailRoute() now has two: a JSON body parser and a multipart
body parser. Since these two middleware functions only run if they recognize the
Content-Type header, only one will assign req.body . That means clients can submit
either format!
routes/emails.js
[···]
[···]
Hop back to Insomnia and duplicate your existing request for POST /emails , but
change the request body type to “Multipart Form” and add a key-value pair for each field
of an email:
In Insomnia, add one more key-value pair with a key of “attachments” and change the
value type to “File”:
File Uploads with Multer 77
Click “Choose…” and pick a file on your hard drive, such as a small image. Want to up-
load more than one attachment with the request? Just add a key-value pair per file with
the same key of “attachments”.
Figure 6.6: Multipart forms can include several files under the same key.
Try sending the request and check the uploads/ directory. If you uploaded three files,
you should see three randomly named files in uploads/ and the JSON response should
include an "attachments" array that corresponds with those filenames:
78 Chapter 6. Common Middleware
Figure 6.7: Uploaded files are stored in uploads/ and listed in the response.
If someone wants to view an email attachment, how do they retrieve it from the back-
end API? We could add a new route like GET /uploads/31760444… for retrieving email
attachments. No need to hand-code those routes, we can use serve-static again to
serve up any files in the uploads/ directory. Pop back to index.js :
index.js
[···]
app.use(logger);
app.use(compress(/* { threshold: 0 } */));
app.use(serveStatic(path.join(__dirname, 'public')));
+ app.use('/uploads', serveStatic(path.join(__dirname, 'uploads')));
app.use('/users', usersRouter);
app.use('/emails', emailsRouter);
[···]
Unlike serving the public/ directory, this time we specified a path prefix as the
first argument to app.use() . Without the prefix, the route would instead be
GET /31760444… .
Create a new request in Insomnia to test a route like GET /uploads/31760444… . De-
pending on the file type you uploaded, the response will probably look like binary gob-
bledygook. Not bad!
Accepting Multiple Body Types 79
Handling file uploads can be tedious in many backend frameworks, but thanks to the
middleware pattern it only takes a few lines of code.
There’s one last thing: when we added multipart support, we broke our old JSON ver-
sion of createEmailRoute() . Try running your old POST /emails request in Insomnia
with a JSON body. The request hangs, and there’s a stack trace in the terminal:
(node:48981) UnhandledPromiseRejectionWarning:
TypeError: Cannot read property 'map' of undefined
at createEmailRoute (routes/emails.js:20:31)
It looks like req.files is undefined instead of an empty array, what’s going on?
body-parser and multer can play nicely with each other because they only run
when the incoming request body has a particular Content-Type header. Since the
req.files property is specific to multer , it will be undefined when the request
body is JSON-formatted.
That’s a simple fix: use the || operator to swap in an empty array just in case:
80 Chapter 6. Common Middleware
routes/emails.js
[···]
[···]
While we’re at it, let’s also send the full URL to each attachment instead of just the file-
name:
routes/emails.js
[···]
[···]
Test out the JSON and multipart versions of the POST /emails request with Insomnia.
Both should work, and the response should always include an "attachments" property!
Figure 6.9: POST /emails works with JSON or multipart request bodies.
Go Further 81
Good middleware doesn’t make too many assumptions about what other middleware
is or is not doing. With just a few lines of code, we added logging, JSON body support,
compression, a frontend and file upload support! That’s the hallmark of middleware:
powerful, reusable behaviors that can be layered without conflicts.
Go Further
Third-party middleware dramatically expands what backends can do with just a few lines
of code. These challenges would have been overwhelming last chapter, but now they’re
just a small stretch!
JSON and multipart forms are pretty awesome, but many clients are used to submitting
URL-encoded bodies like this:
from=1&to=2&subject=Hello&body=World
URL-encoded forms are commonly included as a body or at the end of the URL. Like
multipart forms, they can encode a simple dictionary.
In Insomnia, duplicate your POST /emails request and change the body type to “Form
URL Encoded”. In the Timeline panel, you can see that Insomnia sets the Content-Type
header and generates an ampersand-separated list of key-value pairs.
PATCH Things Up
The PATCH /emails/<id> route for updating existing emails doesn’t accept multipart
forms like POST /emails . Add support and pay close attention to edge cases: how do
you want to handle older attachments that were previously uploaded?
82 Chapter 6. Common Middleware
MIME Types
When attachments are uploaded, the filenames are replaced with random ones, and the
file extension is dropped for security reasons. Unfortunately, that means when some-
one tries to load an attachment in the browser, Express doesn’t know what to set the
Content-Type response header to. Consequently, the browser downloads the attach-
ment rather than displaying it as, for example, an image or PDF.
1. The quick fix is to preserve the file extension: when a client requests an attach-
ment, Express will infer the MIME type from the file extension. However, this is
generally considered a dangerous practice.
2. The better fix is to adjust createEmailRoute() to track not just the filename, but
also the file’s .mimetype property. When a client requests an email attachment,
serveStatic() should search the list of emails for the attachment to find its cor-
responding MIME type and add a Content-Type header.
To inject this behavior, take a look at the setHeaders() option for serve-static .
Part III
83
Chapter 7
Basic Authentication
Right now, Pony Express allows anyone to send and view emails. How can we lock things
down so that only registered users can access Pony Express? To identify which user is
making the request, the backend needs to support user authentication.
Authentication and authorization are two inherently separate concerns that often get
tangled together. Authentication deals with verifying that a user is who they claim they
are. Authorization specifies what that verified user is allowed to do. Strictly separating
authentication and authorization will pave the way for effortless extension, like support-
ing a new authentication method. But a small design shortcut can obscure dangerous
security flaws and frustrate other developers.
In the next few chapters, we won’t cover any particular Node packages, but will instead
focus on good boundaries and design patterns that lay the groundwork for resilient au-
thentication and authorization code, regardless of your tooling choices.
Authorization Header
Let’s start with the simplest form of user authentication: submitting a username and
password. Despite the misleading name, the conventional way to send credentials with a
request is to include an Authorization header.
The Authorization header begins with a word indicating which authentication strat-
egy to use. The strategy is not optional, although many production codebases abuse the
standard by omitting the strategy. There are a handful of standardized authentication
methods available: we’ll start with “Basic”, which is designed for username and password
credentials, and in the next chapter we’ll tackle “Bearer”.
85
86 Chapter 7. Basic Authentication
Everything after the strategy type “Basic” represents the username and password:
bnliYmxyOmFscHM= . The credentials are formatted as username:password , then
converted from ASCII to Base 64. This example represents the string nybblr:alps ,
where nybblr is the username and alps is the password.
In Insomnia, add an Authorization header to your GET /emails request and set the
value to the following:
Basic bnliYmxyOmFscHM=
Make sure you are adding a request header, not using Insomnia’s “Auth” tab. If you want
to authenticate as a different user, you can use Node’s Buffer() constructor to convert
a string to Base 64:
Handling Authentication with Middleware 87
Buffer.from('nybblr:alps').toString('base64');
> "bnliYmxyOmFscHM="
Buffer.from('flurry:redwood').toString('base64');
> "Zmx1cnJ5OnJlZHdvb2Q="
Doing that Base 64 logic is a bit cryptic, so let’s delete the handwritten Authorization
header and select “Basic Auth” in Insomnia’s “Auth” tab. Here, simply type out the
username and password, and Insomnia will automatically generate the same Base 64
encoded Authorization header.
To the backend, nothing changed — Insomnia’s “Auth” tab is just a convenient way to
generate the Authorization header.
Now that we’re including credentials with the request, we need to add support to the
backend. Since most requests should require authentication, middleware is a great fit.
We’ll start writing our own middleware in a new file, lib/basic-auth.js :
88 Chapter 7. Basic Authentication
lib/basic-auth.js
index.js
[···]
const compress = require('compression');
const serveStatic = require('serve-static');
+ const basicAuth = require('./lib/basic-auth');
[···]
app.use(serveStatic(path.join(__dirname, 'public')));
app.use('/uploads', serveStatic(path.join(__dirname, 'uploads')));
+ app.use(basicAuth);
app.use('/users', usersRouter);
app.use('/emails', emailsRouter);
[···]
The placement is important: the user should be identified from their credentials before
the /emails or /users routes run, but authentication isn’t necessary for serving fron-
tend files or email attachments. Arguably, /uploads could use authentication — we will
leave that challenge to you!
We added basicAuth() to the global middleware stack. That’s usually not a great
choice, but most backends should default to requiring authentication for all routes. The
downside is that we’ll need to be especially careful when we implement basicAuth() to
make sure it behaves well with other middleware.
Try running an authenticated GET /emails request with Insomnia. In the terminal, you
should see Basic and bnliYmxyOmFscHM= logged.
Now is a good time to check the authentication strategy: if the type is “Basic”,
basicAuth() should come to life. Otherwise, it should quietly ignore the header and
move on to the next middleware:
Handling Authentication with Middleware 89
lib/basic-auth.js
- console.log(type, payload);
+ if (type === 'Basic') {
+ let credentials = Buffer.from(payload, 'base64').toString('ascii');
+ let [username, password] = credentials.split(':');
+ console.log(username, password);
+ }
next();
};
[···]
Retry an authenticated GET /emails request; the username and password should be
logged in the terminal.
Middleware shouldn’t be tightly coupled to its position in the middleware stack, nor
should it be too eager to blow up. basicAuth() should only blow up if the request opts-
in to “Basic” authentication, but the credentials are wrong. To determine if the creden-
tials are correct, we’ll search the array of users by username and password:
90 Chapter 7. Basic Authentication
lib/basic-auth.js
next();
};
[···]
There are many ways authentication could go wrong, so let’s tabulate a few scenarios:
To handle the last scenario which should not advance to the next middleware, let’s use
an early return to short circuit basicAuth() before it invokes next() :
Graceful Global Middleware 91
lib/basic-auth.js
[···]
+ if (user) {
+ req.user = user;
+ } else {
+ res.sendStatus(401);
+ return;
+ }
}
next();
};
[···]
The most common symptom of global middleware is nested if...else statements. It’s
worth the added complexity in this particular instance, but branch complexity is one of
the most compelling reasons to keep global middleware to a minimum.
Try replicating each of the four scenarios from Insomnia. They should all be working!
Bad credentials should trigger a 401 Unauthorized status code:
Requiring Authentication
Well, except for one thing: if you remove the Authorization header, the backend still
responds to any request. Shouldn’t credentials be mandatory? What’s the point of sup-
porting authentication if it’s optional?
We could make basicAuth() a bit more opinionated and let it blow up if credentials
aren’t included with the request, but this leads to brittle middleware. basicAuth() has
one responsibility: to verify “Basic” credentials. It shouldn’t also have the responsibility
of blowing up if none are included!
Instead, let’s delegate that responsibility to a new middleware. Create a new file,
lib/require-auth.js :
lib/require-auth.js
Where should we add requireAuth() ? There are a couple options, but for now
let’s require authentication on a router-by-router basis. Add requireAuth() to
routes/users.js :
Requiring Authentication 93
routes/users.js
[···]
+ usersRouter.use(requireAuth);
usersRouter.get('/', getUsersRoute);
usersRouter.get('/:id', getUserRoute);
[···]
Surprise! A router generated by express.Router() has its own middleware stack that
runs before any of its routes. Global middleware is generally bad, but repeating middle-
ware per route is tedious, so router-level middleware often hits the sweet spot.
routes/emails.js
[···]
const multer = require('multer');
+ const requireAuth = require('../lib/require-auth');
const generateId = require('../lib/generate-id');
const emails = require('../fixtures/emails');
[···]
+ emailsRouter.use(requireAuth);
emailsRouter.route('/')
[···]
Now all our /users and /emails routes are protected: a user must submit valid cre-
dentials to get a response from the server. Try a few Insomnia requests with authentica-
tion disabled:
94 Chapter 7. Basic Authentication
Basic authentication is feature complete, but there’s one last design problem to tackle:
basicAuth() is hardwired to look up a user by credentials. That lookup logic is specific
to our backend, but good middleware is often easy to reuse in other projects. How could
we remove findUserByCredentials() from this module so basicAuth() can be used
in other backend codebases?
Arrow functions in JavaScript make this style of functional programming a cinch. We just
need to modify the function signature in lib/basic-auth.js :
Currying and Middleware Factories 95
lib/basic-auth.js
[···]
[···]
If you’re new to functional programming or arrow functions with implicit returns, this
might look a typo. We made a function (findUserByCredentials) => that returns our
basicAuth() middleware function. But this time, the rules of variable scoping allow
basicAuth() to memorize the findUserByCredentials argument.
Currying can often be disguised by syntax, but with arrow functions and implicit returns
it’s pretty easy to spot:
add(1, 2, 3);
> 6
add(1)(2)(3);
> 6
96 Chapter 7. Basic Authentication
What’s the point of requiring multiple invocations when it can be done with one? Thanks
to the rules of variable scoping, we can stop at any invocation and save the resulting
function — a partially applied function — to a variable so it can be reused.
add(1)(2)(3);
> 6
addOne(2)(3);
> 6
addOneTwo(3);
> 6
Currying is an elegant way to generate functions that must have a particular signature,
such as (req, res, next) => , but need some extra configuration first. Strictly speak-
ing, currying refers to accepting one argument at a time, but in JavaScript it’s more com-
mon to leave arguments grouped together.
lib/find-user.js
Don’t forget to delete the code you just moved from lib/basic-auth.js :
lib/basic-auth.js
index.js
[···]
const serveStatic = require('serve-static');
const basicAuth = require('./lib/basic-auth');
+ const findUser = require('./lib/find-user');
[···]
Test a few requests in Insomnia one last time to ensure you didn’t break anything.
That subtle change comes with huge design wins! Middleware factories elegantly decou-
ple backend code with almost no consequences for API simplicity. For a complex feature
like authentication, we need all the wins we can get.
With the right design patterns, complex behaviors arise from straightforward code.
Go Further
Hashing Passwords
For simplicity’s sake, we are storing user passwords as plain text in fixtures/users.json ,
but that would be catastrophically unsafe in a production app. Instead, we should hash
the passwords with a library like bcrypt and store the hashed password instead:
98 Chapter 7. Basic Authentication
The hashed password looks like gobbledygook, but most importantly it can’t be reversed
to determine the user’s original password.
> console.log(hashed)
"$2b$10$n77SvPvP/o4eU21J4.cQLfbcqM"
When a user tries to authenticate with their password, use the bcrypt.compare()
method to compare it with the hashed password that’s stored in fixtures/users.json :
If you make these changes in the right place, you’ll be able to follow the next chapter as-
is. Otherwise, you’ll end up duplicating the hashing logic. Choose wisely!
Chapter 8
Sending credentials with each HTTP request is straightforward, but as backends grow,
the surface area for security vulnerabilities also grows. By sending credentials with ev-
ery request, an attacker has plenty of opportunities to compromise user credentials.
Security isn’t the only downside to sending credentials with each request — it also handi-
caps architecture and scaling options. Here are a few examples:
• Every endpoint or server must first verify credentials for each request. That can
quickly become a performance bottleneck: password hashing algorithms like
bcrypt are secure because they are designed to be time consuming — ¼ to one
second. That will noticeably delay every request.
• It’s harder to scale backend services across separate servers because each server
must support user authentication, creating a central bottleneck.
• The backend can’t easily track which devices have used the account or allow the
user to audit and revoke access without resetting their password. Likewise, it’s
difficult to grant restricted access to certain devices.
• It’s difficult to provide alternative authentication methods — such as Single Sign
On (SSO) services — without drastically changing how clients interact with the
API.
None of these are deal breakers early on, and premature optimization is a dangerous
trap. But luckily there’s an easy way to delay these pain points and simplify backend au-
thentication!
Proof of Verification
To cross country borders at an airport, you must prove your identity and citizenship.
One way to do that would be to carry your birth certificate and government-issued ID
with you at all times. However, it is time consuming to verify these documents, and bor-
der control would need access to your home country’s citizen database, plus the exper-
tise to verify those documents.
99
100 Chapter 8. Authentication with JSON Web Tokens
Instead, you present a passport at the border to prove your identity and citizenship. A
passport has security features that make it difficult to tamper with and relatively fast to
verify.
The actual documents still need to be verified, but only when you pick up the passport.
That process could take weeks to months, but it only needs to happen every ten years. If
your passport is stolen, it can be invalidated without compromising your birth certificate
and government-issued ID.
Like a passport, a JSON Web Token (JWT, or simply “token” in this chapter) is a tamper-
resistant document that proves you have verified your identity using credentials like a
username and password. To make authenticated HTTP requests, a client submits their
username and password once to be issued a JWT. On all subsequent HTTP requests, the
client includes the JWT instead of credentials.
Issuing Tokens
First we’ll create a “passport office” at POST /tokens . Create a new router in
routes/tokens.js :
Issuing Tokens 101
routes/tokens.js
index.js
[···]
[···]
app.use('/uploads', serveStatic(path.join(__dirname, 'uploads')));
+ app.use('/tokens', tokensRouter);
app.use(basicAuth(findUser.byCredentials));
[···]
Create a new Insomnia request to POST /tokens and include a JSON-formatted re-
quest body with a username and password:
102 Chapter 8. Authentication with JSON Web Tokens
{
"username": "nybblr",
"password": "alps"
}
The request will hang, but if the credentials are correct, the terminal should print
out the matching user. So once a user proves their identity, what should the
backend respond with? A newly created token! Add an if...else statement to
routes/tokens.js :
routes/tokens.js
[···]
+ if (user) {
+ let token = 'I am user ' + user.id;
+ res.status(201);
+ res.send(token);
+ } else {
+ res.sendStatus(422);
+ }
};
[···]
If the credentials are incorrect, it’s conventional to reply with 422 Unprocessable Entity .
If they are correct, we use the 201 Created status code. But what does a token ac-
tually look like? A token can be anything that identifies the user, such as the string
"I am user 1" or an object like { userId: "1" } . For a token to be useful from a
security perspective, it must be difficult to forge.
Since Pony Express validated the credentials, it should be the only party that can issue
genuine tokens. In other words, Pony Express needs to digitally sign the tokens it issues.
When a client presents this token on a future HTTP request, the backend can quickly tell
if the token is authentic.
Signing Tokens 103
Signing Tokens
This library takes care of the tricky security details behind JSON Web Tokens. To issue a
new token, use the jwt.sign() method:
routes/tokens.js
[···]
1. payload : A plain ol’ JavaScript object with identifying information about the user.
The payload is like the picture page of a passport: it should have enough details
to look up the user’s full profile, but not so much that the token gets long. After all,
104 Chapter 8. Authentication with JSON Web Tokens
the token will need to be sent with every single HTTP request. Most of the time,
the user’s ID is enough.
2. signature : A secret key that only the backend should know. This is the back-
end’s signature for signing all new tokens, so if the signature is compromised,
JWTs can be forged!
3. options : An object of configuration options, such as how long the token is con-
sidered valid. We specified that the token should expire in seven days. After seven
days, the user will need to request a new JSON Web Token by resubmitting their
username and password.
Dissecting a Token
Send a request to POST /tokens with Insomnia. The response should look like a long
string of random characters with a couple periods.
The string isn’t complete gobbledygook. Like the arguments to jwt.sign() , the string
has three sections separated by periods:
1. The first section is a Base 64 string of the algorithm used to generate the token.
With the default algorithm options, the first section encodes a JSON string like
this:
{"alg":"HS256","typ":"JWT"}
2. The middle section is a Base 64 string of the payload — the first argument to
jwt.sign() — along with an issue time and expiration time. The decoded string
looks like this:
Accepting JSON Web Tokens 105
{"userId":"1","iat":1554742821,"exp":1555347621}
3. The last section is a cryptographic signature that proves the first two sections
haven’t been tampered with. Only the backend can generate an authentic digital
signature since it knows the secret signing key.
Luckily, you don’t have to write the code to generate or parse the token. The client sim-
ply presents it with every subsequent HTTP request.
Now that the “passport office” is up and running, the backend needs to support tokens
as an alternative authentication method.
Rather than include an Authorization header with type “Basic” on each request, the
client will use the “Bearer” method. In Insomnia, duplicate your GET /emails request
and change the Auth type from “Basic Auth” to “Bearer Token”. Copy-paste a freshly gen-
erated JWT from your POST /tokens request into the “token” field.
Try sending the request. In the Timeline tab, we see that Insomnia automatically adds an
Authorization header to the request.
We’re still getting a 401 Unauthorized status code because the backend doesn’t sup-
port “Bearer” authentication, but it’s not hard to add. Token authentication will be struc-
turally identical to basicAuth() . Let’s create our own tokenAuth() middleware in
lib/token-auth.js :
106 Chapter 8. Authentication with JSON Web Tokens
lib/token-auth.js
index.js
[···]
const basicAuth = require('./lib/basic-auth');
+ const tokenAuth = require('./lib/token-auth');
const findUser = require('./lib/find-user');
[···]
app.use('/tokens', tokensRouter);
+ app.use(tokenAuth);
app.use(basicAuth(findUser.byCredentials));
[···]
Try sending a JWT-authenticated request to GET /emails . The backend should log an
object with a userId property:
Accepting JSON Web Tokens 107
That’s the payload of your JWT — now Pony Express knows which user sent the request!
How do we know this token is authentic and was issued by Pony Express? If the token
is tampered with, jwt.verify() will throw an exception. Otherwise, it will decode the
Base 64 encoding on the payload, parse it as JSON and return it.
As with basicAuth() , we should look up the user’s details and store them in req.user
for the routes. To keep our middleware reusable, let’s pass the payload object to a func-
tion called findUserByToken() that searches by the userId property:
lib/token-auth.js
- console.log(payload);
next();
};
[···]
108 Chapter 8. Authentication with JSON Web Tokens
If there is a matching user, tokenAuth() will store the user in req.user to signal to
requireAuth() that the user successfully authenticated. Try your JWT-authenticated
GET /emails request — your old “Basic” authentication should still work too!
There’s one edge case we need to handle. In the “Auth” tab for your GET /emails re-
quest, try tampering with the token string — for example, delete a character from the
end of the token and retry the request.
The backend should spit out a long stack trace in the terminal, such as a JsonWebTokenError :
That’s mostly good: the jsonwebtoken module noticed that the token was tampered
with. However, the server shouldn’t crash with 500 Internal Server Error — to
other developers, that indicates a bug. Servers should make every effort to handle
expected errors, so let’s catch the error and respond with 401 Unauthorized instead:
Decoupling with Middleware Factories 109
lib/token-auth.js
[···]
next();
};
[···]
Retry the GET /emails request with the tampered token. The server should politely re-
spond with a 401 Unauthorized status code instead of 500 Internal Server Error .
Like basicAuth() , the tokenAuth() function is due for some refactoring. Move
findUserByToken() into lib/find-user.js :
lib/find-user.js
[···]
exports.byCredentials = findUserByCredentials;
+ exports.byToken = findUserByToken;
110 Chapter 8. Authentication with JSON Web Tokens
index.js
[···]
app.use('/tokens', tokensRouter);
- app.use(tokenAuth);
+ app.use(tokenAuth(findUser.byToken));
app.use(basicAuth(findUser.byCredentials));
[···]
Last, delete the code for findUserByToken() from lib/token-auth.js and make
tokenAuth() a middleware factory:
lib/token-auth.js
[···]
Try a few requests from Insomnia just to make sure everything still works with both au-
thentication methods.
The backend now seamlessly supports two authentication methods: username and
password with “Basic”, and JSON Web Tokens with “Bearer”. Because we designed
basicAuth() to gracefully ignore anything other than “Basic” authentication and left
enforcement to requireAuth() , we were able to support a second authentication
mechanism like tokenAuth() with one line of middleware. Most importantly, we didn’t
need to modify existing code and potentially introduce security regressions!
Go Further 111
Go Further
Environment Variables
Source code is not a safe place to hide sensitive information. “Magic values” like
signature are hardcoded into the backend’s source code, so anyone who gets a copy
of the code will know the signature and can quietly issue valid tokens to impersonate
any user.
It’s a better idea to extract the signature into an environment variable so it never ap-
pears in the source code. This variable needs to be specified every time the backend is
booted:
During development, it can be tedious to list out environment variables just to boot up
the server. The dotenv module lets you list out these variables in a separate file called
.env :
.env
SIGNATURE=1m_s3cure
This .env file should never be committed to source code. To load it, the backend needs
to run the dotenv module as early as possible:
index.js
require('dotenv').config();
Nothing else needs to change! Boot up the backend as before, without listing all the en-
vironment variables:
112 Chapter 8. Authentication with JSON Web Tokens
Find another “magic value” to extract from the source code — for example, the
expiresIn value — and load it from an environment variable instead.
Chapter 9
Pony Express supports two authentication methods, but it’s still not particularly secure:
any client with valid user credentials can do anything, such as deleting another user’s
emails.
Authentication deals with verifying that a user is who they claim to be. Authorization
specifies what that verified user is allowed to do. Is user #1 allowed to edit an email they
drafted? Can user #2 spy on an email they didn’t author or receive?
Modify your Insomnia request for PATCH /emails/1 and DELETE /emails/1 to use
Basic authentication with credentials for user #3. A user shouldn’t be able to edit an
email they didn’t author or delete an email not addressed to them, but both of these re-
quests currently work.
113
114 Chapter 9. Authorization Design Patterns
routes/emails.js
[···]
[···]
Retry your PATCH /emails/1 and DELETE /emails/1 requests with credentials for
user #3. This time, both should respond with a 403 Forbidden status code.
Our goal is to eliminate this structural duplication while making the code readable. It’s
only fair to warn you that the in-between steps won’t be pretty, so roll up your sleeves
and get ready for some dirt!
A great way to extract an if...else statement from a route is by moving it into middle-
ware. Let’s add a dedicated middleware function for both routes whose sole responsibil-
ity is to guard the route:
routes/emails.js
[···]
[···]
routes/emails.js
[···]
[···]
routes/emails.js
[···]
emailsRouter.route('/:id')
.get(getEmailRoute)
- .patch(bodyParser.json(), updateEmailRoute)
+ .patch(
+ authorizeUpdateEmailRoute,
+ bodyParser.json(),
+ updateEmailRoute
+ )
- .delete(deleteEmailRoute)
+ .delete(
+ authorizeDeleteEmailRoute,
+ deleteEmailRoute
+ )
;
[···]
Our routes are short and focused again: after rolling back the changes to updateEmailRoute()
and deleteEmailRoute() , we extracted the authorization logic into dedicated route
middleware. But there’s still duplication between authorizeUpdateEmailRoute() and
authorizeDeleteEmailRoute() .
These two middleware functions share the same logic, except for one difference: the
access rules. That’s because there are two unique roles tangled together: the policy and
enforcer.
• The policy specifies the actual authorization rules with a simple yes or no answer
to the question, “can this user do that?”
• The enforcer makes sure that policy is respected: it either continues to the next
middleware or responds with 403 Forbidden .
routes/emails.js
[···]
[···]
routes/emails.js
[···]
[···]
The two authorize*() functions are almost identical now. The only difference is the
policy they enforce. Let’s turn one of them into a middleware factory called enforce()
that can be reused with different policies:
routes/emails.js
[···]
[···]
[···]
120 Chapter 9. Authorization Design Patterns
routes/emails.js
[···]
emailsRouter.route('/:id')
.get(getEmailRoute)
.patch(
- authorizeUpdateEmailRoute,
+ enforce(updateEmailPolicy),
bodyParser.json(),
updateEmailRoute
)
.delete(
- authorizeDeleteEmailRoute,
+ enforce(deleteEmailPolicy),
deleteEmailRoute
)
;
[···]
The enforce() middleware factory belongs in its own module now. Move it to a new
file called lib/enforce.js :
lib/enforce.js
routes/emails.js
[···]
const requireAuth = require('../lib/require-auth');
const generateId = require('../lib/generate-id');
+ const enforce = require('../lib/enforce');
const emails = require('../fixtures/emails');
[···]
[···]
Simplifying Policies
What about the email lookup logic we had to duplicate between updateEmailRoute()
and updateEmailPolicy() ? Could we simplify the policy functions so there is less
room for duplication bugs?
There are several ways to tackle this, but here’s one that’s loosely based on a Ruby autho-
rization library called Pundit. Imagine if each route could decide when to apply autho-
rization logic so it could pass along the exact email to authorize:
122 Chapter 9. Authorization Design Patterns
routes/emails.js
[···]
[···]
[···]
Let’s dream a bit more. Suppose that req.authorize() could in turn invoke the appro-
priate policy and pass along the authenticated user and email. Our policies would be-
come substantially tidier:
Simplifying Policies 123
routes/emails.js
[···]
[···]
[···]
Policies that are just a line long? Yes! To make req.authorize() a reality, the
enforce() middleware factory can add the method to the request object in
lib/enforce.js :
124 Chapter 9. Authorization Design Patterns
lib/enforce.js
[···]
Instead of running the policy immediately, enforce() adds a method to req to defer
that logic until the route decides to invoke it. By then, the route can look up the email
and pass it to the policy as an argument.
Test out PATCH /emails and DELETE /emails with Insomnia. They should all work as
before!
Well, everything works with one exception: even though unauthorized users get a
403 Forbidden response, the edit or deletion still happens! You can confirm this bug
with a GET /emails request.
Even though the request seems to be denied, the route continued executing. There is
even a stack trace in the terminal from the route trying to respond after enforce() al-
ready closed the connection. That’s because req.authorize() responds to the request,
but it doesn’t prevent the rest of the route function from executing.
How could req.authorize() force the route to exit without a bunch of if...else
statements? This is exactly what exceptions are for! Let’s make a custom Error type in
lib/enforce.js :
Sustainable Security 125
lib/enforce.js
next();
};
[···]
Errors force the function that called req.authorize() to exit prematurely. It may
seem strange to throw errors on purpose, but exceptions are simply a different kind of
return value — one that bubbles up through functions.
Try PATCH /emails and DELETE /emails again — this time, the changes should never
happen.
Sustainable Security
Our backend has come a long way over the last few chapters. Authentication and autho-
rization are easily some of the most complex features every backend will tackle, so it’s
crucial to delay that complexity with opinionated design.
A completely secure backend isn’t technically possible, but we can ward off many attacks
by eliminating the primary source of vulnerabilities: regressions in complex code. A se-
cure codebase is useless if new developers can’t easily imitate or modify it for new func-
tionality.
Go Further
There are many more routes that need authorization logic. Start by adding a policy for
getEmailRoute() , then add a policy for createEmailRoute() . This one is trickier: a
user shouldn’t be able to create an email that is not from them.
Private Attachments
Now that getEmailRoute() is secured, viewing an attachment should follow the same
access rules. Add a policy to the /uploads routes that only allows the recipient or au-
thor who uploaded the attachment to view it.
- app.use('/uploads', serveStatic(...));
+ app.use('/uploads', enforce(emailAttachmentPolicy), serveStatic(...));
Index
abstraction, 3 middleware, 56
Authentication, 85 error handling middleware, 65
Authorization, 85, 113 Middleware Factory, 70
middleware stack, 56
client, see also user agent multipart form data, 74
cohesion, 3
cross cutting concerns, 58 nodemon, 18
CRUD, 48 npx, 18
127