Just Express (with a bunch of node and http). In detail.
- General Reference
- Helpful Prerequisite Knowledge
- Express is just a node module
- Connecting to the cloud (i.e., what "the cloud" is)
- Data packets
- TCP and UDP (brief overviews)
- HTML, HTTP, and the stateless nature of HTTP
- HTTP messages (start line, header, body; request/response)
- Start Line
- Node server without Express (the basics/fundamentals)
- Node server without Express (routing and serving up static files)
- Comparing Express and Plain Node: Simple Server, Routing, etc.
- Middleware Basics
- Rendering Basics
- Express usage: API development and/or server-side rendering (
res.json
and/orres.render
, respectively) - View Engines
- Rendering
- A preface about rendering in general
- A note about
res.locals
and passing data inres.render
- Passing data that we trust (example of unescaping HTML using EJS)
- Include other files for more robust templating (example using EJS)
- Adding styles, scripts, etc., to template files (properly setting
href
,src
, etc.) - Basic examples (using EJS, Handlebars, and Pug)
- Express usage: API development and/or server-side rendering (
- Request and Response Objects: Revisited
- Forms: getting data from the
request
object - Cookies: using
res.cookie
,res.clearCookie
, and other cookie-related information - Redirects: the optional
status
value inres.redirect([status,] path)
- Passing data: the query string (using
req.query
) - Passing data: parameters via URL wildcards (using
req.params
andapp.param()
) - Passing data: sending files (via
res.download
) and dealing withheaders already sent
error
- Forms: getting data from the
- Miscellany: Advanced Routing, Express Generator, HTTP Headers, etc.
General Reference
Purpose of Express
The main job for Express is to manage HTTP traffic (i.e., manage how the request and response go back and forth). Hence, it makes sense to first talk about what HTTP even is and that relies in part on understanding TCP (transmission control protocol) and UDP (user datagram protocol).
API reference outline (4.x)
- express()
- Application
- Request
- Properties
- Methods
- Response
- Properties
- Methods
- Router
Helpful Prerequisite Knowledge
Express is just a node module
Express is "just" a node module. Literally, you can install Express via NPM because Express is literally just a node module. The Express website makes clear what Express is:
Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
It also makes clear how, implicitly or explicitly, the main job for Express is to manage HTTP traffic via methods on the request
and response
objects:
With a myriad of HTTP utility methods and middleware at your disposal, creating a robust API is quick and easy.
Connecting to the cloud (i.e., what "the cloud" is)
The cloud is not a cloud but just a network of computers (not belonging to you) talking with each other where the language they speak is in "packets" where these packets are little streams of data. See the Wiki entry on cloud computing (first sentence):
Cloud computing is the on-demand availability of computer system resources, especially data storage (cloud storage) and computing power, without direct active management by the user.
Some other relevant terminological excerpts related to the above excerpt:
- Cloud storage: Cloud storage is a model of computer data storage in which the digital data is stored in logical pools, said to be on "the cloud". The physical storage spans multiple servers (sometimes in multiple locations), and the physical environment is typically owned and managed by a hosting company. These cloud storage providers are responsible for keeping the data available and accessible, and the physical environment protected and running. People and organizations buy or lease storage capacity from the providers to store user, organization, or application data. Cloud storage services may be accessed through a colocated cloud computing service, a web service application programming interface (API) or by applications that utilize the API, such as cloud desktop storage, a cloud storage gateway or Web-based content management systems."
- Pool (computer science): In computer science, a pool is a collection of resources that is kept ready to use, rather than acquired on use and released afterwards. In this context, resources can refer to system resources such as file handles, which are external to a process, or internal resources such as objects. A pool client requests a resource from the pool and performs desired operations on the returned resource. When the client finishes its use of the resource, it is returned to the pool rather than released and lost. The pooling of resources can offer a significant response-time boost in situations that have high cost associated with resource acquiring, high rate of the requests for resources, and a low overall count of simultaneously used resources. Pooling is also useful when the latency is a concern, because a pool offers predictable times required to obtain resources since they have already been acquired. These benefits are mostly true for system resources that require a system call, or remote resources that require a network communication, such as database connections, socket connections, threads, and memory allocation. Pooling is also useful for expensive-to-compute data, notably large graphic objects like fonts or bitmaps, acting essentially as a data cache or a memoization technique. Special cases of pools are connection pools, thread pools, and memory pools.
Data packets
The data interchange between client and server happens through packets of data called network packets. Relevant Wiki excerpts provided below:
- Network packet: In telecommunications and computer networking, a network packet is a formatted unit of data carried by a packet-switched network. A packet consists of control information and user data; the latter is also known as the payload. Control information provides data for delivering the payload (e.g., source and destination network addresses, error detection codes, or sequencing information). Typically, control information is found in packet headers and trailers. In packet switching, the bandwidth of the transmission medium is shared between multiple communication sessions, in contrast to circuit switching, in which circuits are preallocated for the duration of one session and data is typically transmitted as a continuous bit stream.
- Payload: In computing and telecommunications, the payload is the part of transmitted data that is the actual intended message. Headers and metadata are sent only to enable payload delivery. In the context of a computer virus or worm, the payload is the portion of the malware which performs malicious action. The term is borrowed from transportation, where payload refers to the part of the load that pays for transportation.
When you deal with Express, you (i.e., the server) are now put in charge of handling what packets of data people (i.e., the client or browser) receive:
So you are now in charge of effectively serving up that content. Node does most of the heavy lifting via low-level C. As can be seen above in the illustration, a "data packet" consists of 5 parts or layers. From the lowest-level to the highest-level, the following are the layers of each packet:
- Physical: Cables. These are the actual physical cables connecting things together. See the submarine cable map for more details and the end of this note for two visuals (the map of cables and a randomly chosen specific cable).
- Link: Wifi or ethernet connection
- Internet/Network: IP (Internet Protocol)
- Transport: UDP/TCP
- Application: HTTP, FTP, SSH, SMTP
The following are more specific examples from Wiki:
- Application layer: FTP, HTTP, HTTPS, SSH, NTP, etc.
- Transport layer: TCP, UDP, DCCP, SCTP, etc.
- Internet layer: IP (IPv4 and IPv6), ICMP, ICMPv6, ECN, etc.
- Link layer: MAC (Ethernet, Wi-Fi, DSL, ISDN, FDDI), NDP, ARP, etc.
The network and transport layers together form the internet protocol suite or TCP/IP.
Wiki note on Internet protocol suite |
---|
The Internet protocol suite is the conceptual model and set of communications protocols used in the Internet and similar computer networks. It is commonly known as TCP/IP because the foundational protocols in the suite are the Transmission Control Protocol (TCP) and the Internet Protocol (IP). During its development, versions of it were known as the Department of Defense (DoD) model because the development of the networking method was funded by the United States Department of Defense through DARPA. Its implementation is a protocol stack. The Internet protocol suite provides end-to-end data communication specifying how data should be packetized, addressed, transmitted, routed, and received. This functionality is organized into four abstraction layers, which classify all related protocols according to the scope of networking involved. From lowest to highest, the layers are the link layer, containing communication methods for data that remains within a single network segment (link); the internet layer, providing internetworking between independent networks; the transport layer, handling host-to-host communication; and the application layer, providing process-to-process data exchange for applications. The technical standards underlying the Internet protocol suite and its constituent protocols are maintained by the Internet Engineering Task Force (IETF). The Internet protocol suite predates the OSI model, a more comprehensive reference framework for general networking systems. |
Developers generally spend the majority of their time in the application, transport, and internet layers, with most time being spent specifically in the application layer. Express only handles HTTP requests but it's important to note that the HTTP application layer uses the transport layer and specifically TCP instead of UDP.
Submarine cable map
Here is a picture of the entire cable map as of March 27, 2020:
Particular cable from the submarine cable map
Here is a specific cable from the submarine cable map (note it is 11,000km in length and where its landing points are):
TCP and UDP (brief overviews)
You have a computer with an internet connection. The transport layer creates 216 = 65,536 ~ 65k ports on your computer. Whenever you start a Node app, say on port 3000, the reason you have that port 3000 at all is because you are using one of the 65k ports that the transport layer creates. If you started an app on port 5000 (like a Flask app or Rails app), then again you are using one of those ports.
Think of your network connection as being a hotel, where the hotel is a single building but with tons of individual rooms that are all numbered. If someone comes to the hotel, then in order to find a guest, they need to know the room number. Armed with the room number, they can actually find who they are looking for.
Typically what happens is an application of a given machine will issue a network request. Suppose this request is an HTTP request. And suppose the request originates from port 49742 (an arbitrary port of the 65k available). Let's suppose the request wants to talk with port 80 on another computer. That request will get handed off to the transport layer and that will get wrapped up in what's called a segment. Inside of the segment there will be metadata which will have the destination port (i.e., port 80) and the source report (i.e., port 49742). The transport layer will hand that off to the network layer for further processing. When it gets to the receiving machine, it will go through the process in reverse and eventually find the right port.
There are two different kinds of transport layer protocols: UDP and TCP. They can broadly be characterized in the following manner:
UDP (User Datagram Protocol)
Basically the win is that UDP is crazy fast but the loss is that it is incredibly unreliable. Since Express is based on HTTP and not UDP, Express will not have these faults (but it will also miss out on some of the positives). Here's the high-level view of UDP:
- Lightweight: Only 8 bytes for a header. Very little overhead required to work.
- Connectionless: If a client wants to talk with a server, then you do not have to create a connection first. You can go ahead and start sending data from the client without a connection to a server being established.
- Consistency: UDP is good and bad.
- Bad: UDP will send data no matter what. This may seem good on the surface but it can also be quite bad. What if there's packet loss? UDP doesn't care. It will keep right on sending packets. It doesn't make any difference. What if the network is very congested? It doesn't care. It will just keep right on sending packets and just make the network more and more congested. What if the packets are out of order? It doesn't care. It's not UDP's problem. They're just going to show up out of order--that will be the other side's problem.
- Good: Everything mentioned above is bad. So what is good about UDP? What's the win? It's blazing fast. It's very lightweight (the headers are incredibly small). You don't have to bother to set up a connection to start. You can just start sending data. It's consistent in how it sends data. Packets will always show up whether they are ordered properly or not.
- Use cases: UDP is primarily used for things like video games or real-time communication. If you have ever experienced "lag" in a video game where everything seems to stop or go back in time and then suddenly catch up...that's UDP. That's the client screaming at the server without making a connection. And suddenly the server updates your machine with "Whoops, you're actually way behind. I'm going to start sending some different data."
TCP (Transmission Control Protocol)
Connection-based: Unlike UDP, if you are a client and you want to start talking with a computer via TCP (i.e., if you are a browser and want to start communicating with a server), then you don't just start screaming and sending data as in UDP. You have to go through what's called a 3-way handshake (the "TLS Handshake" or "Transport Layer Security Handshake" you have probably seen before when waiting for a webpage to load). Before you are going to transmit any data, you are going to have to initiate a connection. The 3-way handshake goes like this:
- The client says, "Hey, I'd like to talk."
- The server responds with yes or no. Hopefully the server responds with yes and that it is happy to set up a connection.
- Actual data starts being transmitted.
These are the 3 steps that will happen before a TCP connection actually goes through.
Reliable: From the above, we can see TCP is reliable because we actually know the connection is going to happen before any data is transmitted. Additionally, for TCP, there are data acknowledgments. What this means is that every time data is transmitted the server will let the client know that it received the client's data and vice-versa. There's also retransmission of data in TCP. If data isn't received, then the server can let the client know (and vice-versa) that some data was not received and the client can send it again.
Ordered packets: With UDP, there may be packet loss or disorganized packets. With TCP, you can guarantee that the packets arrive in the correct order regardless of what happens with the network.
Congestion control: With TCP, if the network is overwhelmed, it may intentionally introduce latency to try and keep packet loss to a minimum to not make the problem worse.
The upshot of all of this is to use TCP when you need reliability and probably UDP when you need something fast and you don't need it to be reliable.
What you need to remember: TCP and IP, together, get two computers ready to talk with each other. They create an environment that will allow two computers to talk with each other. And HTTP uses TCP as the transport layer because it is reliable whereas UDP is not.
HTML, HTTP, and the stateless nature of HTTP
What do HTTP and HTML have in common? The first two letters: Hyper Text. Something fun to check out: info.cern.ch. This was the very first webpage that was ever made. It's not just HTML. It was the magic that was being able to get all of the networking happening together to be able to pass the HTML around. But HTTP doesn't just pass around HTML anymore. HTTP definitely still passes around HTML, but it also passes around images, 4k videos, etc.
Some highlights about HTTP as a protocol:
- Efficiency: It is incredibly efficient. TCP remains connected. It connects and then remains connected until all of the data has been sent. HTTP does not have to stay open. HTTP is only connected when absolutely necessary. Once the request arrives, the machines will disconnect entirely from each other. As soon as the responder is ready, the HTTP connection will reestablish across TCP and will send the response.
- Stateless: No dialogue. This means the machines only know about each other for as long as the connection is open. As soon as the connection closes, everything is completely forgotten. So if they need to talk again for any reason, they have to start over completely again, which is like to say it's the very first conversation (i.e., the 3-way handshake needs to occur: tell me who you are and what you want). It's not like a phone conversation where the first person says something (request) and listens for a response and there's a running memory throughout of what is being said back and forth (i.e., there's a history and different points that can be looked back to and referenced to inform and continue the dialogue). Stateless means, "I only know about what you just said right now. And I'm going to respond based on that and then completely forget everything." So the requestor gets one thing to say and the responder gets one thing to say and then they are totally done talking.
Illustration of how this process works in practice: Suppose you have a user on a computer who is connected to the internet. Let's say they go to Udemy's website. Here's the process:
- The user is going to go through their internet connection (through their ISP) and they'll bounce around however many times before eventually getting to the host machine, Udemy's servers, via TCP and IP. They will go through the process of establishing that 3-way handshake. This is the first step. TCP says the client (i.e., the user's browser) would like to make a connection to the server (i.e., Udemy's servers).
- Via TCP, the server will respond in the affirmative that a connection can be made.
- Then the data will start to come. Part of that data is going to be the HTTP request. So the HTTP request will come in to the server and once the request has hit, then that connection is terminated. The TCP connection is still open but the HTTP request has terminated. The client computer is still patiently waiting for some kind of response. It still wants an HTTP response. While the server does its song and dance, it will get an HTTP response together and send back the response. The connection will then be terminated. And then the TCP connection is also terminated and everything goes away.
That's the basic gist of how the networking will interact and how the HTTP messages will go back and forth. What does an HTTP message actually look like though? See the next note.
HTTP messages (start line, header, body; request/response)
There are three parts to an HTTP message:
- Start line
- Header
- Body
HTTP messages are generally all text so you usually can read an HTTP message. Let's unpack each piece of the above.
Start Line
The start line is a single line, and it describes the type of request on the way there (i.e., what kind of action or method to which resource endpoint or path) and on the way back in the response it's the status.
- Request:
method
|path
|version-of-HTTP
; for example:get /blog HTTP/1.1
- Response:
version-of-HTTP
|status-code
; for example:HTTP/1.1 404
Header
The header specifies the request or response and describes its body (discussed below). So essentially what the header contains is metadata. And it always comes in the form of key-value pairs. So as a JS developer, it will look like an object or JSON. There are loads of options in there and the earliest we will use is a mime-type. For example, we might see something like content-type: text/html
. There will always be a blank line between the header and body. And that is to indicate that all of the header is done and that it's time for the body.
MDN has a great article about MIME types, an excerpt from which appears below.
MDN note about MIME types |
---|
A media type (also known as a Multipurpose Internet Mail Extensions or MIME type) is a standard that indicates the nature and format of a document, file, or assortment of bytes. It is defined and standardized in IETF's RFC 6838. The Internet Assigned Numbers Authority (IANA) is responsible for all official MIME types, and you can find the most up-to-date and complete list at their Media Types page. Browsers use the MIME type, not the file extension, to determine how to process a URL, so it's important that web servers send the correct MIME type in the response's Content-Type header. If this is not correctly configured, browsers are likely to misinterpret the contents of files and sites will not work correctly, and downloaded files may be mishandled. |
An answer on Stack Overflow also sheds nice light on what a MIME type is (reproduced below).
Stack Overflow answer about what a MIME type is exactly |
---|
A MIME type is a label used to identify a type of data. It is used so software can know how to handle the data. It serves the same purpose on the Internet that file extensions do on Microsoft Windows. So if a server says "This is text/html" the client can go "Ah, this is an HTML document, I can render that internally", while if the server says "This is application/pdf" the client can go "Ah, I need to launch the FoxIt PDF Reader plugin that the user has installed and that has registered itself as the application/pdf handler." You'll most commonly find them in the headers of HTTP messages (to describe the content that an HTTP server is responding with or the formatting of the data that is being POSTed in a request) and in email headers (to describe the message format and attachments). |
Body
The "actual stuff" or data of the request or response or what you may think of as maybe the content or HTML, the image, etc.
Recap and Example |
---|
All of the above is what makes up HTTP messages. You have to follow that protocol, namely having a start line, header, and body. |
Example: We can use something like curl
to illustrate the above with curl -v www.google.com
in Bash to get the following (comments added to make what is printed to the console more sensible):
# Notes that curl gives us are offset by * markers (this isn't part of the HTTP request):
* Rebuilt URL to: www.google.com/
* Trying 2607:f8b0:4009:80d::2004...
* TCP_NODELAY set
* Connected to www.google.com (2607:f8b0:4009:80d::2004) port 80 (#0)
# Start line (of request)
> GET / HTTP/1.1 # GET method; path requested is / (i.e., the root); and protocol is HTTP/1.1
# Headers (of request)
> Host: www.google.com
> User-Agent: curl/7.54.0
> Accept: */*
# End of request headers
>
# In our request, we didn't actually send a body because it's a GET request
# Note the meaning of the left-most arrows: > correspond to request and < to response
# Start line (of response)
< HTTP/1.1 200 OK
# Headers (of response)
< Date: Thu, 02 Apr 2020 05:08:30 GMT
< Expires: -1
< Cache-Control: private, max-age=0
< Content-Type: text/html; charset=ISO-8859-1
...
< Accept-Ranges: none
< Vary: Accept-Encoding
< Transfer-Encoding: chunked
# End of response headers
<
# Response body
<!doctype html>
...
Node server without Express (the basics/fundamentals)
See immediately below for the TLDR version (this is explained in detail in the rest of this note).
TLDR Version
Here is the TLDR version of possibly the most basic Node server without Express:
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, {'content-type': 'text/html'});
res.write('<h1>Hello, World!</h1>');
res.end();
});
server.listen(3000);
Note |
---|
It is always a good idea to consult the docs through the Node website. There you will find documentation about all things Node. For example, under HTTP you will see the following: "To use the HTTP server and client one must require('http') ." |
In setting up a Node server without Express, we use the following (all of which can be read up on through the links to the docs as noted above):
Class | Syntax | Documentation |
---|---|---|
N/A | http.createServer([options][, requestListener]) | Link |
http.ServerResponse | response.writeHead(statusCode[, statusMessage][, headers]) | Link |
http.ServerResponse | response.write(chunk[, encoding][, callback]) | Link |
http.ServerResponse | response.end([data[, encoding]][, callback]) | Link |
http.Server | server.listen() | Link |
The HTTP module is simply part of Node--it is not a third-party module (like Express) that we need to install with NPM or Yarn or something like that. In fact, as noted in the docs, the core modules (e.g., http
, path
, etc.) are compiled into the binary; that is, the core modules are defined within the Node.js source code which means if you have Node.js installed then you definitely have the core modules installed. You still have to require
the core modules to use them (e.g., require('http')
), they're baked into the source code of Node.js so you don't have to reach out to NPM or some other source! One core module of many is the http
module and this is the module that will allow us to make HTTP requests and responses (it has those request and response objects for us to interact with, typically denoted by req
and res
, respectively). The HTTP module has a createServer
method that comes with it.
Note from the docs about createServer |
---|
In the docs we see the syntax http.createServer([options][, requestListener]) which indicates we are expected to pass createServer a requestListener function, where the requestListener is a function which is automatically added to the request event, and http.createServer returns a new instance of http.Server , which is itself an extension of net.Server , a class used to create a TCP or IPC server. |
createServer
As noted above, the requestListener
function passed to http.createServer
is automatically added to the request
event, an event that is emitted each time there is a request to the server (where there may be multiple requests per connection such as in the case of HTTP Keep-Alive connections). The docs entry for request
begin in the following manner:
request
<http.IncomingMessage>
response
<http.ServerResponse>
The docs note that the http.IncomingMessage
object extends the stream.Readable
class, is created by http.Server
or http.ClientRequest
, and is passed as the first argument to the request
and response
event respectively. It may be used to access response status, headers, and data.
The docs note that the http.ServerResponse
class object extends Stream and is an object created internally by an HTTP server (not by the user). It is passed as the second parameter to the request
event.
In simple terms, the createServer
method takes one argument, a callback function, where this callback function takes two arguments, the request
and response
objects, respectively, which are typically denoted as req
and res
to avoid potential naming conflicts while using other node modules (e.g., the now-deprecated request
node module).
The request
and response
objects, or req
and res
, are just that (i.e., what was noted earlier about HTTP traffic and how requests and responses work): they represent the HTTP request message and the HTTP response message, respectively. The req
object represents what we know about the requesting machine or the HTTP request that has come in. And we actually know quite a lot about it. Because we need to. We need to be able to get back to them. We need to be able to find them. So their IP address will be in there, we'll know something about their client (i.e., what type of browser it was and stuff like that), what page or route they wanted to find, all the stuff in the headers, whether or not there was any data passed to us, etc. There will be lots of stuff inside of the request object.
The response
object is what we are going to send back. And, generally speaking, in Express, we are going to get stuff out of the request
object, and we are going to add stuff to the response
object. That is somewhat of an oversimplification, but with strict Node we have fairly limited options (at least in the sense of not being a major pain). Using Express will make our lives much easier.
Before we log anything to the console to see anything about a request or possibly putting together a response, we need to be able to listen for traffic on the server so we can respond appropriately. Fittingly, createServer
returns an object with a listen
method, where listen
has the following syntax for TCP servers according to the docs:
server.listen([port[, host[, backlog]]][, callback])
As the docs note, the listen
function is asynchronous, and when the server starts listening the listening
event will be emitted. If a callback
function is passed to listen
, then the callback
will be added as a listener for the listening
event. Hence, it's fairly common to see something like the following:
const http = require('http');
const port = 3000;
const server = http.createServer((req, res) => {
// Stuff
});
server.listen(port, () => console.log(`Server listening on port ${port}!`));
The callback
passed to listen
in this case is really just a sanity check to make sure the server is listening and that we've started things up effectively.
Note about the port number |
---|
The number 3000 is not special. Neither is 5000 nor 8000 nor several other arbitrary port numbers (within the 65k possible ports). But there are some exceptions. The port number has to be greater than 1000 because unless your role is root then you do not have access to ports 1000 and below unless you change the permissions which is not often advised. |
Recap: The HTTP module is native to Node--we do not have to install it. We simply have to ask for it in the form of const http = require('http');
. We create a server
. We use the HTTP module to create the server with http.createServer
, where createServer
is a function that takes a callback which will run whenever an HTTP request is made to the server. When is an HTTP request made to the server? It is made whenever the port
on which the server is listening gets an HTTP request. We can try this out with the following simple program:
const http = require('http');
const server = http.createServer((req, res) => {
console.log(req);
});
server.listen(3000);
Execute this with Node and then open the browser and go to localhost:3000
. You should see an enormous object logged to the console.
Partial req
object logged
You'll see a ton of different things but here's an example of a few of the things:
IncomingMessage {
_readableState: ReadableState {
objectMode: false,
highWaterMark: 16384,
buffer: BufferList { head: null, tail: null, length: 0 },
length: 0,
pipes: null,
pipesCount: 0,
flowing: null,
ended: false,
...
httpVersionMajor: 1,
httpVersionMinor: 1,
httpVersion: '1.1',
complete: false,
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
'cache-control': 'max-age=0',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36',
'sec-fetch-dest': 'document',
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'sec-fetch-site': 'none',
'sec-fetch-mode': 'navigate',
'sec-fetch-user': '?1',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'en-US,en;q=0.9,la;q=0.8'
},
rawHeaders: [
'Host',
'localhost:3000',
'Connection',
'keep-alive',
'Cache-Control',
'max-age=0',
'Upgrade-Insecure-Requests',
'1',
'User-Agent',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36',
'Sec-Fetch-Dest',
'document',
'Accept',
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Sec-Fetch-Site',
'none',
'Sec-Fetch-Mode',
'navigate',
'Sec-Fetch-User',
'?1',
'Accept-Encoding',
'gzip, deflate, br',
'Accept-Language',
'en-US,en;q=0.9,la;q=0.8'
],
trailers: {},
rawTrailers: [],
aborted: false,
upgrade: false,
url: '/',
method: 'GET',
statusCode: null,
statusMessage: null,
client: Socket {
connecting: false,
_hadError: false,
...
If you look at the above snippet for the req
object, then you'll see stuff like the headers
object which is a bunch of key-value pairs like host: 'localhost:3000'
, what the user-agent
is, etc. We are not interested in all of this right now. The point is that we get a ton of information about the HTTP request that's being made in the form of an IncomingMessage
.
With the above code, if you actually made the request, then you may see your browser having a spinning wheel or hanging. What's happening? Basically, the browser is waiting for a response. We fielded the request, but we never sent anything back. And that's a problem for the browser because the browser needs a response to know that we are actually finished. This is where the response
object, or res
, comes in. It will be our way of responding to the requestor.
Before putting together the response
object ourselves, recall the following about what each HTTP message (request or response) is comprised of:
- start line: Node will take care of this for us.
- header: We need to deal with this in Node even though Express will largely take care of this for us in the future.
- body: We are absolutely in charge of this. It's a pain to handle in Node but will be much easier in Express.
Let's put together our response message:
Start line
Node takes care of this for us so we do not need to worry about attaching a start line to the res
object to send back to the requestor.
Header
The response
object has a writeHead
method we can use which takes two arguments, the first being a status code and the second being an object for the mime-type. We can do something like the following:
res.writeHead(200, {'content-type': 'text/html'});
This will write out our headers, and this is where we attach the header to the res
object to send back to the requestor.
Body
The response
object has a write
method we can use and we can pass it some HTML to be used as the body for the response:
res.write('<h1>Hello, World!</h1>');
This is where we attach the body to the res
object to send back to the requestor.
Great! We have now attached a start line, header, and body to the res
object (really Node automatically attached the start line and we attached the header and body by means of res.writeHead
and res.write
, respectively). But what do we actually do with the response object now? How do we send it back to the requesting agent (most often a browser)? We finally need to use something like res.end()
at the very end because we need to let the browser know to close the connection.
So here's the final stripped down Node web server without Express:
// nodeServer.js
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, {'content-type': 'text/html'});
res.write('<h1>Hello, World!</h1>');
res.end();
});
server.listen(3000);
If we now run curl -v localhost:3000
, we'll get something like the following:
* Rebuilt URL to: localhost:3000/
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET / HTTP/1.1 # start line (request)
> Host: localhost:3000 # header
> User-Agent: curl/7.54.0 # header
> Accept: */* # header
> # end of start line/headers (and no body)
< HTTP/1.1 200 OK # start line (response)
< content-type: text/html # header (response)
< Date: Thu, 02 Apr 2020 08:55:29 GMT # header (inserted by Node)
< Connection: keep-alive # header (inserted by Node)
< Transfer-Encoding: chunked # header (inserted by Node)
< # end of start line/headers
* Connection #0 to host localhost left intact # inserted by Node
<h1>Hello, World!</h1> # body (response)
Very cool!
Note about routing |
---|
Our little "Hello, World!" message will show up regardless of the path the user tries to visit because we are simply listening on port 3000 for HTTP traffic. Whenever there is traffic, the code above says hey send this response right along. In nodeServerTwo.js , we add a conditional to say what to do based on certain routing (i.e., we will have something specific happen for the home page, etc.) |
Node server without Express (routing and serving up static files)
TLDR |
---|
The driving point here is that serving up routes and static files in plain Node without Express is horrible. It is no fun at all. But as observed at the end of the previous note, we have to have some way of selectively choosing when something renders and when something doesn't. We do not want everything to render for a site just when somebody visits the root. Hence, we have to introduce routing of some sort. |
Recall the most basic of Node servers from the previous note:
// nodeServer.js
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/html' });
res.write('<h1>Hello, World!</h1>');
res.end();
})
server.listen(3000);
For the web server illustrated above, no matter what kind of request is issued (e.g., get
, put
, post
, delete
) or to what endpoint or route (e.g., /books
, /books?author=Arthur
, /crazyyy
), we will always respond with '<h1>Hello, World!</h1>'
. But of course this isn't really the desired behavior--what we really want is to selectively respond based on what kind of request the user is issuing (e.g., simply get
ting a web page, post
ing a review, etc.) and to what endpoint or route (i.e., post
ing to the /reviews
route should result in something different than post
ing to the /comments
route).
How can we selectively send back a response based on what kind of request the user issued as well as to which path or route they issued the request? To effectively answer this question, we really need to know more about the request object itself. We can gain a better understanding of the request object if we inspect the entire object ourselves. To do this, create a new file, nodeServerWithLoggedRequest.js
, and make the following adjustments to the basic server in nodeServer.js
:
// nodeServerWithLoggedRequest.js
const http = require('http');
const path = require('path');
const fs = require('fs');
const util = require('util');
const server = http.createServer((req, res) => {
if (req.url !== '/favicon.ico') {
fs.writeFile(path.join(__dirname, 'sampleRequest'), util.inspect(req), err => console.log(err));
}
res.writeHead(200, { 'content-type': 'text/html' });
res.write('<h1>Hello, World!</h1>');
res.end();
})
server.listen(3000);
Some notes about the modifications above:
path
: We make use of thepath
module: "Thepath
module provides utilities for working with file and directory paths." Specifically, we use thepath.join
method to effectively bring together the folder where the Node process is currently executing (i.e.,__dirname
) and what we want our file to be called that we are going to write data to (i.e.,sampleRequest
):path.join(__dirname, 'sampleRequest')
.fs
: We make use of thefs
module: "Thefs
module enables interacting with the file system in a way modeled on standard POSIX functions." Specifically, we use thefs.writeFile
method to write data from the request object to a file.util
: We make use of theutil
module: "Theutil
module supports the needs of Node.js internal APIs. Many of the utilities are useful for application and module developers as well." Specifically, we make use of theutil.inspect()
method. As noted on Stack Overflow and further in theutil
docs,util.inspect(obj)
automatically replaces circular links with[Circular]
. This is important because there are circular references on the request object, and if you try the above withoututil.inspect(req)
and justreq
, then you will end up with a file containing only[object Object]
. The next thought may be to try to useJSON.stringify(req)
, but you will then end up with the following error message:TypeError: Converting circular structure to JSON
. This is becauseJSON.stringify
has two exceptions: It will throw aTypeError
when either a circular reference is found (as in our case) or when trying to stringify aBigInt
value. There you have it. Usingutil.inspect
is an easy workaround.if
statement: The statementif (req.url !== '/favicon.ico') { ... }
is simply meant to ensure that only the data we want will be written to file. As noted here, "Browsers will by default try to request/favicon.ico
from the root of a hostname, in order to show an icon in the browser tab." We don't care about the request for the favicon, hence theif
statement.
With the modifications above in mind, start listening on the server and issue a GET
request to the following URL (i.e., simply visit the following URL): http://localhost:3000/books?author=Arthur&title=Wow
. The full request object is shown below (over 700 lines!).
Sample req
object logged to console after request
IncomingMessage {
_readableState: ReadableState {
objectMode: false,
highWaterMark: 16384,
buffer: BufferList { head: null, tail: null, length: 0 },
length: 0,
pipes: null,
pipesCount: 0,
flowing: null,
ended: false,
endEmitted: false,
reading: false,
sync: true,
needReadable: false,
emittedReadable: false,
readableListening: false,
resumeScheduled: false,
emitClose: true,
autoDestroy: false,
destroyed: false,
defaultEncoding: 'utf8',
awaitDrain: 0,
readingMore: true,
decoder: null,
encoding: null,
[Symbol(kPaused)]: null
},
readable: true,
_events: [Object: null prototype] {
end: [Function: resetHeadersTimeoutOnReqEnd]
},
_eventsCount: 1,
_maxListeners: undefined,
socket: Socket {
connecting: false,
_hadError: false,
_parent: null,
_host: null,
_readableState: ReadableState {
objectMode: false,
highWaterMark: 16384,
buffer: BufferList { head: null, tail: null, length: 0 },
length: 0,
pipes: null,
pipesCount: 0,
flowing: true,
ended: false,
endEmitted: false,
reading: true,
sync: false,
needReadable: true,
emittedReadable: false,
readableListening: false,
resumeScheduled: false,
emitClose: false,
autoDestroy: false,
destroyed: false,
defaultEncoding: 'utf8',
awaitDrain: 0,
readingMore: false,
decoder: null,
encoding: null,
[Symbol(kPaused)]: false
},
readable: true,
_events: [Object: null prototype] {
end: [Array],
timeout: [Function: socketOnTimeout],
data: [Function: bound socketOnData],
error: [Function: socketOnError],
close: [Array],
drain: [Function: bound socketOnDrain],
resume: [Function: onSocketResume],
pause: [Function: onSocketPause]
},
_eventsCount: 8,
_maxListeners: undefined,
_writableState: WritableState {
objectMode: false,
highWaterMark: 16384,
finalCalled: false,
needDrain: false,
ending: false,
ended: false,
finished: false,
destroyed: false,
decodeStrings: false,
defaultEncoding: 'utf8',
length: 0,
writing: false,
corked: 0,
sync: true,
bufferProcessing: false,
onwrite: [Function: bound onwrite],
writecb: null,
writelen: 0,
afterWriteTickInfo: null,
bufferedRequest: null,
lastBufferedRequest: null,
pendingcb: 0,
prefinished: false,
errorEmitted: false,
emitClose: false,
autoDestroy: false,
bufferedRequestCount: 0,
corkedRequestsFree: [Object]
},
writable: true,
allowHalfOpen: true,
_sockname: null,
_pendingData: null,
_pendingEncoding: '',
server: Server {
insecureHTTPParser: undefined,
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
_connections: 2,
_handle: [TCP],
_usingWorkers: false,
_workers: [],
_unref: false,
allowHalfOpen: true,
pauseOnConnect: false,
httpAllowHalfOpen: false,
timeout: 120000,
keepAliveTimeout: 5000,
maxHeadersCount: null,
headersTimeout: 40000,
_connectionKey: '6::::3000',
[Symbol(IncomingMessage)]: [Function: IncomingMessage],
[Symbol(ServerResponse)]: [Function: ServerResponse],
[Symbol(kCapture)]: false,
[Symbol(asyncId)]: 3
},
_server: Server {
insecureHTTPParser: undefined,
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
_connections: 2,
_handle: [TCP],
_usingWorkers: false,
_workers: [],
_unref: false,
allowHalfOpen: true,
pauseOnConnect: false,
httpAllowHalfOpen: false,
timeout: 120000,
keepAliveTimeout: 5000,
maxHeadersCount: null,
headersTimeout: 40000,
_connectionKey: '6::::3000',
[Symbol(IncomingMessage)]: [Function: IncomingMessage],
[Symbol(ServerResponse)]: [Function: ServerResponse],
[Symbol(kCapture)]: false,
[Symbol(asyncId)]: 3
},
timeout: 120000,
parser: HTTPParser {
'0': [Function: parserOnHeaders],
'1': [Function: parserOnHeadersComplete],
'2': [Function: parserOnBody],
'3': [Function: parserOnMessageComplete],
'4': [Function: bound onParserExecute],
_headers: [],
_url: '',
socket: [Circular],
incoming: [Circular],
outgoing: null,
maxHeaderPairs: 2000,
_consumed: true,
onIncoming: [Function: bound parserOnIncoming],
parsingHeadersStart: 0
},
on: [Function: socketListenerWrap],
addListener: [Function: socketListenerWrap],
prependListener: [Function: socketListenerWrap],
_paused: false,
_httpMessage: ServerResponse {
_events: [Object: null prototype],
_eventsCount: 1,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
_last: false,
chunkedEncoding: false,
shouldKeepAlive: true,
useChunkedEncodingByDefault: true,
sendDate: true,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: null,
_hasBody: true,
_trailer: '',
finished: false,
_headerSent: false,
socket: [Circular],
connection: [Circular],
_header: null,
_onPendingData: [Function: bound updateOutgoingData],
_sent100: false,
_expect_continue: false,
[Symbol(kCapture)]: false,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: null
},
[Symbol(asyncId)]: 10,
[Symbol(kHandle)]: TCP {
reading: true,
onconnection: null,
_consumed: true,
[Symbol(owner)]: [Circular]
},
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: Timeout {
_idleTimeout: 120000,
_idlePrev: [Timeout],
_idleNext: [TimersList],
_idleStart: 4629,
_onTimeout: [Function: bound ],
_timerArgs: undefined,
_repeat: null,
_destroyed: false,
[Symbol(refed)]: false,
[Symbol(asyncId)]: 11,
[Symbol(triggerId)]: 10
},
[Symbol(kBuffer)]: null,
[Symbol(kBufferCb)]: null,
[Symbol(kBufferGen)]: null,
[Symbol(kCapture)]: false,
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0
},
connection: Socket {
connecting: false,
_hadError: false,
_parent: null,
_host: null,
_readableState: ReadableState {
objectMode: false,
highWaterMark: 16384,
buffer: BufferList { head: null, tail: null, length: 0 },
length: 0,
pipes: null,
pipesCount: 0,
flowing: true,
ended: false,
endEmitted: false,
reading: true,
sync: false,
needReadable: true,
emittedReadable: false,
readableListening: false,
resumeScheduled: false,
emitClose: false,
autoDestroy: false,
destroyed: false,
defaultEncoding: 'utf8',
awaitDrain: 0,
readingMore: false,
decoder: null,
encoding: null,
[Symbol(kPaused)]: false
},
readable: true,
_events: [Object: null prototype] {
end: [Array],
timeout: [Function: socketOnTimeout],
data: [Function: bound socketOnData],
error: [Function: socketOnError],
close: [Array],
drain: [Function: bound socketOnDrain],
resume: [Function: onSocketResume],
pause: [Function: onSocketPause]
},
_eventsCount: 8,
_maxListeners: undefined,
_writableState: WritableState {
objectMode: false,
highWaterMark: 16384,
finalCalled: false,
needDrain: false,
ending: false,
ended: false,
finished: false,
destroyed: false,
decodeStrings: false,
defaultEncoding: 'utf8',
length: 0,
writing: false,
corked: 0,
sync: true,
bufferProcessing: false,
onwrite: [Function: bound onwrite],
writecb: null,
writelen: 0,
afterWriteTickInfo: null,
bufferedRequest: null,
lastBufferedRequest: null,
pendingcb: 0,
prefinished: false,
errorEmitted: false,
emitClose: false,
autoDestroy: false,
bufferedRequestCount: 0,
corkedRequestsFree: [Object]
},
writable: true,
allowHalfOpen: true,
_sockname: null,
_pendingData: null,
_pendingEncoding: '',
server: Server {
insecureHTTPParser: undefined,
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
_connections: 2,
_handle: [TCP],
_usingWorkers: false,
_workers: [],
_unref: false,
allowHalfOpen: true,
pauseOnConnect: false,
httpAllowHalfOpen: false,
timeout: 120000,
keepAliveTimeout: 5000,
maxHeadersCount: null,
headersTimeout: 40000,
_connectionKey: '6::::3000',
[Symbol(IncomingMessage)]: [Function: IncomingMessage],
[Symbol(ServerResponse)]: [Function: ServerResponse],
[Symbol(kCapture)]: false,
[Symbol(asyncId)]: 3
},
_server: Server {
insecureHTTPParser: undefined,
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
_connections: 2,
_handle: [TCP],
_usingWorkers: false,
_workers: [],
_unref: false,
allowHalfOpen: true,
pauseOnConnect: false,
httpAllowHalfOpen: false,
timeout: 120000,
keepAliveTimeout: 5000,
maxHeadersCount: null,
headersTimeout: 40000,
_connectionKey: '6::::3000',
[Symbol(IncomingMessage)]: [Function: IncomingMessage],
[Symbol(ServerResponse)]: [Function: ServerResponse],
[Symbol(kCapture)]: false,
[Symbol(asyncId)]: 3
},
timeout: 120000,
parser: HTTPParser {
'0': [Function: parserOnHeaders],
'1': [Function: parserOnHeadersComplete],
'2': [Function: parserOnBody],
'3': [Function: parserOnMessageComplete],
'4': [Function: bound onParserExecute],
_headers: [],
_url: '',
socket: [Circular],
incoming: [Circular],
outgoing: null,
maxHeaderPairs: 2000,
_consumed: true,
onIncoming: [Function: bound parserOnIncoming],
parsingHeadersStart: 0
},
on: [Function: socketListenerWrap],
addListener: [Function: socketListenerWrap],
prependListener: [Function: socketListenerWrap],
_paused: false,
_httpMessage: ServerResponse {
_events: [Object: null prototype],
_eventsCount: 1,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
_last: false,
chunkedEncoding: false,
shouldKeepAlive: true,
useChunkedEncodingByDefault: true,
sendDate: true,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: null,
_hasBody: true,
_trailer: '',
finished: false,
_headerSent: false,
socket: [Circular],
connection: [Circular],
_header: null,
_onPendingData: [Function: bound updateOutgoingData],
_sent100: false,
_expect_continue: false,
[Symbol(kCapture)]: false,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: null
},
[Symbol(asyncId)]: 10,
[Symbol(kHandle)]: TCP {
reading: true,
onconnection: null,
_consumed: true,
[Symbol(owner)]: [Circular]
},
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: Timeout {
_idleTimeout: 120000,
_idlePrev: [Timeout],
_idleNext: [TimersList],
_idleStart: 4629,
_onTimeout: [Function: bound ],
_timerArgs: undefined,
_repeat: null,
_destroyed: false,
[Symbol(refed)]: false,
[Symbol(asyncId)]: 11,
[Symbol(triggerId)]: 10
},
[Symbol(kBuffer)]: null,
[Symbol(kBufferCb)]: null,
[Symbol(kBufferGen)]: null,
[Symbol(kCapture)]: false,
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0
},
httpVersionMajor: 1,
httpVersionMinor: 1,
httpVersion: '1.1',
complete: false,
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
'cache-control': 'max-age=0',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36',
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'sec-fetch-site': 'none',
'sec-fetch-mode': 'navigate',
'sec-fetch-user': '?1',
'sec-fetch-dest': 'document',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'en-US,en;q=0.9,la;q=0.8'
},
rawHeaders: [
'Host',
'localhost:3000',
'Connection',
'keep-alive',
'Cache-Control',
'max-age=0',
'Upgrade-Insecure-Requests',
'1',
'User-Agent',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36',
'Accept',
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Sec-Fetch-Site',
'none',
'Sec-Fetch-Mode',
'navigate',
'Sec-Fetch-User',
'?1',
'Sec-Fetch-Dest',
'document',
'Accept-Encoding',
'gzip, deflate, br',
'Accept-Language',
'en-US,en;q=0.9,la;q=0.8'
],
trailers: {},
rawTrailers: [],
aborted: false,
upgrade: false,
url: '/books?author=Arthur&title=Wow',
method: 'GET',
statusCode: null,
statusMessage: null,
client: Socket {
connecting: false,
_hadError: false,
_parent: null,
_host: null,
_readableState: ReadableState {
objectMode: false,
highWaterMark: 16384,
buffer: BufferList { head: null, tail: null, length: 0 },
length: 0,
pipes: null,
pipesCount: 0,
flowing: true,
ended: false,
endEmitted: false,
reading: true,
sync: false,
needReadable: true,
emittedReadable: false,
readableListening: false,
resumeScheduled: false,
emitClose: false,
autoDestroy: false,
destroyed: false,
defaultEncoding: 'utf8',
awaitDrain: 0,
readingMore: false,
decoder: null,
encoding: null,
[Symbol(kPaused)]: false
},
readable: true,
_events: [Object: null prototype] {
end: [Array],
timeout: [Function: socketOnTimeout],
data: [Function: bound socketOnData],
error: [Function: socketOnError],
close: [Array],
drain: [Function: bound socketOnDrain],
resume: [Function: onSocketResume],
pause: [Function: onSocketPause]
},
_eventsCount: 8,
_maxListeners: undefined,
_writableState: WritableState {
objectMode: false,
highWaterMark: 16384,
finalCalled: false,
needDrain: false,
ending: false,
ended: false,
finished: false,
destroyed: false,
decodeStrings: false,
defaultEncoding: 'utf8',
length: 0,
writing: false,
corked: 0,
sync: true,
bufferProcessing: false,
onwrite: [Function: bound onwrite],
writecb: null,
writelen: 0,
afterWriteTickInfo: null,
bufferedRequest: null,
lastBufferedRequest: null,
pendingcb: 0,
prefinished: false,
errorEmitted: false,
emitClose: false,
autoDestroy: false,
bufferedRequestCount: 0,
corkedRequestsFree: [Object]
},
writable: true,
allowHalfOpen: true,
_sockname: null,
_pendingData: null,
_pendingEncoding: '',
server: Server {
insecureHTTPParser: undefined,
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
_connections: 2,
_handle: [TCP],
_usingWorkers: false,
_workers: [],
_unref: false,
allowHalfOpen: true,
pauseOnConnect: false,
httpAllowHalfOpen: false,
timeout: 120000,
keepAliveTimeout: 5000,
maxHeadersCount: null,
headersTimeout: 40000,
_connectionKey: '6::::3000',
[Symbol(IncomingMessage)]: [Function: IncomingMessage],
[Symbol(ServerResponse)]: [Function: ServerResponse],
[Symbol(kCapture)]: false,
[Symbol(asyncId)]: 3
},
_server: Server {
insecureHTTPParser: undefined,
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
_connections: 2,
_handle: [TCP],
_usingWorkers: false,
_workers: [],
_unref: false,
allowHalfOpen: true,
pauseOnConnect: false,
httpAllowHalfOpen: false,
timeout: 120000,
keepAliveTimeout: 5000,
maxHeadersCount: null,
headersTimeout: 40000,
_connectionKey: '6::::3000',
[Symbol(IncomingMessage)]: [Function: IncomingMessage],
[Symbol(ServerResponse)]: [Function: ServerResponse],
[Symbol(kCapture)]: false,
[Symbol(asyncId)]: 3
},
timeout: 120000,
parser: HTTPParser {
'0': [Function: parserOnHeaders],
'1': [Function: parserOnHeadersComplete],
'2': [Function: parserOnBody],
'3': [Function: parserOnMessageComplete],
'4': [Function: bound onParserExecute],
_headers: [],
_url: '',
socket: [Circular],
incoming: [Circular],
outgoing: null,
maxHeaderPairs: 2000,
_consumed: true,
onIncoming: [Function: bound parserOnIncoming],
parsingHeadersStart: 0
},
on: [Function: socketListenerWrap],
addListener: [Function: socketListenerWrap],
prependListener: [Function: socketListenerWrap],
_paused: false,
_httpMessage: ServerResponse {
_events: [Object: null prototype],
_eventsCount: 1,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
_last: false,
chunkedEncoding: false,
shouldKeepAlive: true,
useChunkedEncodingByDefault: true,
sendDate: true,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: null,
_hasBody: true,
_trailer: '',
finished: false,
_headerSent: false,
socket: [Circular],
connection: [Circular],
_header: null,
_onPendingData: [Function: bound updateOutgoingData],
_sent100: false,
_expect_continue: false,
[Symbol(kCapture)]: false,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: null
},
[Symbol(asyncId)]: 10,
[Symbol(kHandle)]: TCP {
reading: true,
onconnection: null,
_consumed: true,
[Symbol(owner)]: [Circular]
},
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: Timeout {
_idleTimeout: 120000,
_idlePrev: [Timeout],
_idleNext: [TimersList],
_idleStart: 4629,
_onTimeout: [Function: bound ],
_timerArgs: undefined,
_repeat: null,
_destroyed: false,
[Symbol(refed)]: false,
[Symbol(asyncId)]: 11,
[Symbol(triggerId)]: 10
},
[Symbol(kBuffer)]: null,
[Symbol(kBufferCb)]: null,
[Symbol(kBufferGen)]: null,
[Symbol(kCapture)]: false,
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0
},
_consuming: false,
_dumped: false,
[Symbol(kCapture)]: false
}
If you do a quick search for "Circular" in the request object shown above, then you will see there are 15 instances found (mostly having to do with sockets, connections, and symbols). In any case, how does this help us with our problem concerning how to come up with an appropriate response? First of all, note what the req
object looks like: IncomingMessage { ... information about the request ... }
. What does IncomingMessage
mean? As noted in the docs about http.IncomingMessage
: "An IncomingMessage
object is created by http.Server
or http.ClientRequest
and passed as the first argument to the 'request'
and 'response'
event respectively. It may be used to access response status, headers and data."
Our IncomingMessage
is created by http.Server
. How do we know this? The docs on http.createServer
make this clear with the following concluding line: "Returns a new instance of http.Server
." What kind of information belongs to this IncomingMessage
and, furthermore, could any of this information be of use to us? The information belonging to the IncomingMessage
object has everything to do with the request issued by a user to our server.
Within the IncomingMessage
object, do a search for "method" and you will get 1 result. In our example, we have the following: method: 'GET'
. This tells us that a GET
request was issued. Furthermore, look at the url
property above the method
property: url: '/books?author=Arthur&title=Wow'
. Based on this information, you can imagine putting together a server that responded in different ways based on what its method
value was as well as its url
value. Since we are using only Node and not Express just yet, we will not make use of req.method
, but we can put together a basic server with customized responses based on req.url
(i.e., what endpoint the user issues a request to):
const http = require('http'); // enable ability to manage HTTP traffic
const fs = require('fs'); // access THIS computer's file system (yours, not the requestor's) with Node
const server = http.createServer((req, res) => {
console.log(`Requested URL: ${req.url}`); // observe what URL is requested
if (req.url === '/') { // user wants the home page
res.writeHead(200, { 'content-type': 'text/html' });
const homePageHTML = fs.readFileSync('node.html');
res.write(homePageHTML);
res.end();
} else if (req.url === "/node.png") {
res.writeHead(200, { 'content-type': 'image/png' });
const image = fs.readFileSync('./node.png');
res.write(image);
res.end();
} else if (req.url === "/styles.css") {
res.writeHead(200, { 'content-type': 'text/css' });
const css = fs.readFileSync('./styles.css');
res.write(css);
res.end();
} else {
res.writeHead(404, { 'content-type': 'text/html' });
res.write(`<h4>Sorry, this isn't the page you're looking for!</h4>`)
res.end()
}
});
server.listen(3000);
Quick disclaimer about the above code: It may seem like we are "serving up" different files, but that is not what we are doing. We are serving up a response. So what we are going to do is read from a file, and then we're going to send the contents of that file back to the requestor. (The file's contents we will attach to our response will be node.html
, shown below.)
The node.png
file is just the logo on the Node.js Wiki page, and the styles.css
file is very basic:
body {
text-align: center;
}
img {
max-width: 200px;
}
To really understand what's going on though, we need to take a look at the basic node.html
file:
<!-- node.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="./styles.css"> <!--This requires an HTTP request-->
<title>Home Page</title>
</head>
<body>
<div class="container">
<h1>Node.js is an open-source, cross-platform JavaScript run-time environment that executes JavaScript code outside of a browser.</h1>
<img src="./node.png" alt="Picture of Node"> <!--This requires an HTTP request-->
</div>
</body>
</html>
The server design will become abundantly clear when we start our server up and visit http://localhost:3000
. We see the following logged to the console:
Requested URL: /
Requested URL: /styles.css
Requested URL: /node.png
What is happening here? The /
indicates a request to the root of our host, and we can verify this by means of using curl
:
bash-3.2$ curl -v localhost:3000
* Rebuilt URL to: localhost:3000/
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: text/html
< Date: Sat, 03 Oct 2020 16:14:04 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="./styles.css"> <!--This requires an HTTP request-->
<title>Home Page</title>
</head>
<body>
<div class="container">
<h1>Node.js is an open-source, cross-platform JavaScript run-time environment that executes JavaScript code outside of a browser.</h1>
<img src="./node.png" alt="Picture of Node"> <!--This requires an HTTP request-->
</div>
</body>
* Connection #0 to host localhost left intact
</html>
Quick aside: Note that the url
property on the request object (i.e., req.url
) is the path relative to the root domain that the request hit--since our root domain is http://localhost:3000
, req.url
will be everything after that. So if the user went to http://localhost:3000/books/book?title=Andronicus&author=Blanksy
, for example, then req.url
would be /books/book?title=Andronicus&author=Blanksy
. More can be read about req.url
by visiting the docs for http.IncomingMessage
(since the req
object is an IncomingMessage
as is made clear when logging the req
object which looks like IncomingMessage { ... }
, as noted previously) and then jumping to message.url
: "[message.url
is] only valid for request obtained from http.Server
[which is true in our case]. [message.url
stands for the] request URL string. This contains only the URL that is present in the actual HTTP request."
If we simply visit http://localhost:3000
, then what will req.url
be? It will simply be /
. Why? The curl
request above illustrates this--note how near the top we have * Rebuilt URL to: localhost:3000/
. So whenever an HTTP request is made, a /
gets added if there isn't one, and this simply indicates a request to the root of the host domain if /
is all there is.
If we revisit the node.html
code above, we can start to make sense of what is going on with our more advanced albeit still basic web server: We first make a request to /
and get back the HTML present in the node.html
file, but there are two lines in the node.html
file worth looking at in more detail:
<link rel="stylesheet" href="./styles.css">
<img src="./node.png" alt="Picture of Node">
We essentially have three requests:
/
: The browser starts to process thenode.html
file whose contents we have written to the response object to send back to the requestor. When the browser is processing and gets to the line<link rel="stylesheet" href="./styles.css">
, what happens? This line leads back to our server (i.e.,./
indicates a relative path to the location of our server), which means we are going to have another HTTP request. So the initial request to/
runs because someone (i.e., the browser) made a request to port 3000. Then our callback inside ofcreateServer
begins executing, following the directions laid out for whenreq.url === '/'
. The browser then gets its response and starts doing its thing (i.e., processing thetext/html
we sent it from thenode.html
file whose contents we read in). As soon as it hits the line<link rel="stylesheet" href="./styles.css">
, a new request comes in to port 3000. Will thestyles.css
stylesheet be loaded if we don't have routing logic for it based onreq.url
? No! Why? Because our server is not a file server. We're not sending files back. So what happens? We respond based on whatever routing logic we have--if we don't have routing built for a request to/styles.css
, then either nothing will happen or we'll get a 404 message if we have that configured. The server does not send back files--it serves back responses./styles.css
: The request is made to this endpoint as soon as the browser hits the line<link rel="stylesheet" href="./styles.css">
. The./
indicates the request should go tohttp://localhost:3000
or, more specifically,http://localhost:3000/styles.css
./node.png
: The request is made to this endpoint as soon as the browser hits the line<img src="./node.png" alt="Picture of Node">
. Again,./
indicates the request should go tohttp://localhost:3000
or, more specifically,http://localhost:3000/node.png
.
In summary, three separate HTTP requests are needed, and this was evident when logging req.url
to the console when using our web server. But now we know why three separate requests were issued. It should now be painfully clear how burdensome this would be to build out an entire application like this. So much toiling for basic functionality! Express will help enormously in regards to all of this.
Comparing Express and Plain Node: Simple Server, Routing, etc.
What is Express and why should we care (i.e., how does it help with Node)?
If we visit the Express website, then we are greeted with the following:
Express: Fast, unopinionated, minimalist web framework for Node.js
Cool. But what does this mean? Well, let's first make it clear that Express is literally "just" a node module. That is, we cannot have Express without Node even though we can have Node without Express (as painful as that might be).
The "fast" part is debatable because everything is fast until it's not. But Express has put in a lot of work to trim things down and make it as lean and lightweight as possible. The "unopinionated" is a market-friendly word but sometimes it's good and sometimes it's bad. Basically, "unopinionated" means they don't force things on you. Rails is basically the opposite--they make decisions for you like sort of corralling you into using Postgres. The "framework" element is mostly a remark on how the Express architects have tried to make everything that one might commonly use when employing Node in an application.
There are a lot of reasons to use Express. One of the obvious reasons (or it will be obvious) is all of the utilities Express gives us to avoid a lot of the torment we had to endure previously when trying to serve things up and routing stuff in Node. Web applications can be made quickly and easily with Express. There are lots of templating engines for Express like Jade/Pug, EJS, Handlebars, etc. Express truly shines in building APIs. It's almost unfair how quickly and easily you can build one and process JSON and respond with JSON.
Basic Express server without routing (reworking the Node server in Express)
Recall the most basic Node server we set up previously:
// nodeServer.js
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, {'content-type': 'text/html'});
res.write('<h1>Hello, World!</h1>');
res.end();
})
server.listen(3000);
We will now recreate the above basic server in Express.
Recall that previously we could just write const http = require('http');
because the HTTP module is native to Node. Well, the Express module is not native to Node. It is a third-party module and thus we need to install it with NPM. So we need to have a package.json
file in our folder structure.
Installing a new node module and where it is saved |
---|
When you install a new node module, it is going to install itself relative to the first package.json it finds. It will put itself in the node_modules folder. |
The best way to handle this is to run npm init
inside of whatever folder your project is in where your node modules should be located (generally at the root-level).
Since we want to use the Express module, we will execute npm install express
which then adds a bunch of different dependencies to the node_modules
folder. Subsequently, we can use the Express module just as we use other modules:
const express = require('express');
const app = express();
In almost all Express applications, you will include the line const app = express();
as well. What do these lines do? Well, the express
variable declared above holds whatever has been exported by the express
node module. If you look in the node_modules
folder for express
, then you can inspect it to see what is being exported (from its index.js
file). We find the following in index.js
:
module.exports = require('./lib/express');
So now we open the lib
folder inside of the express
module and look at the express.js
file. Lots of stuff is being exported but the thing we are interested in is this line which contains what is being exported by default:
exports = module.exports = createApplication;;
What is createApplication
? It is a function in that file:
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
// expose the prototype that will get set on requests
app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
// expose the prototype that will get set on responses
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
app.init();
return app;
}
So basically writing const express = require('express');
is equivalent to writing const express = createApplication;
. So when we write const app = express();
what we are really doing is invoking the createApplication
function. What's the return value of this function? It is app
(with everything Express makes possible on app
).
The point of all of this, which may seem like just a bunch of rigmarole at first, is to train ourselves to think about what is really going on under the hood so we can better inspect problems we may encounter and to better understand boilerplate syntax.
As noted in the docs, the Express app
comes with a whole bunch of methods, one of which is all
, which takes two arguments, the first being a route or path and the second being a callback function to invoke if the path specified as the first argument is requested, where the callback accepts three arguments: req
, res
, and next
. The first two we are already acquainted with and the next
one we will become more acquainted with when we start looking at middleware.
Let's use app.all
in a very generic way:
app.all('*', (req, res, next) => {
res.send(`<h1>This is the home page!</h1>`)
})
Some things to note here: The '*'
means we will listen for HTTP traffic for any route on the specified port, much as we did previously. The very first win is that Express handles the basic headers (i.e., status code, mime-type; we may have to modify them once we get fancy) which is awesome. Another awesome win is that Express handles closing the connection so we do not manually need to do something like res.end()
. What we need to deal with is the in-between. We need to come up with the response
body we want to send back to the requestor. As the docs note, we will not use res.write
but res.send
. Finally, the last thing we need to do, as noted in the docs, is app.listen
instead of server.listen
.
In sum, we can create a basic server in either Node or Express.
In Node:
// nodeServer.js
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, {'content-type': 'text/html'});
res.write('<h1>Hello, World!</h1>');
res.end();
})
server.listen(3000);
In Express:
// expressServer.js
const express = require('express');
const app = express();
app.all('*', (req, res, next) => {
res.send(`<h1>This is the home page!</h1>`)
})
app.listen(3000);
Basic Express routing concepts and implementation
We will recreate the plain Node server that served static files (or the content of those files, that is) in a bit but we should get some basic routing concepts down from Express.
To get started, we can immediately include the express
module since we are inside the same directory in which express
was installed before and we can go ahead and plan to listen on port 3000:
const express = require('express');
const app = express();
// routing and other stuff
app.listen(3000);
As can be seen in the docs, app
has a ton of methods (but we are especially interested right now in the ones in bold):
app.all()
app.delete()
app.disable()
app.disabled()
app.enable()
app.enabled()
app.engine()
app.get()
app.get()
app.listen()
app.METHOD()
app.param()
app.path()
app.post()
app.put()
app.render()
app.route()
app.set()
app.use()
We are interested in the bolded ones because they correspond to HTTP verbs! REST verbs! Worth noting is that when you make an HTTP request you are making a specific type of HTTP request. We can easily see these methods correspond to what many people would think of as a CRUD application where you can create, read, update, and delete, all of which correspond to app.post
, app.get
, app.put
, and app.delete
, respectively. (The app.all
method simply accepts any type of request.)
Of course, a GET
request is the default for all browsers. This is why a tool like Postman is so useful. You can make all sorts of requests besides a GET
request (even though you can do that too of course).
So the application methods we want to focus on right now are the following:
app.all()
app.delete()
app.get()
app.post()
app.put()
Each of these methods takes two arguments, the first being a route or path and the second being a callback function to invoke if an HTTP request is made to the first argument (i.e., the route or path) with a verb that matches the application method name for that route; that is, something like
app.post('/post-something', (req, res, next) => {
res.send(`<h1>I tried to POST something!</h1>`)
})
indicates that we are looking out for an HTTP request to the post-something
route where the HTTP request is specifically a POST
request.
The great thing is we can handle all types of requests on the same path/route with very little overhead. The callbacks for the routes will only respond when a request is made to the specified path and the request is of a type equivalent to the application method being used as above with post
. This is great news!
The point here is that the routing system in Express is meant to handle two things, namely the type of HTTP request and also the path you actually want to fetch (req.method
and req.url
in plain Node, respectively). The application methods are named to correspond with the HTTP verbs they are looking out for.
Express works from the top down. That is, as soon as we have sent a response, subsequent matching routes won't get run (unless we explicitly architect it in a way to do this).
Basic Express server with routing (reworking the Node server in Express)
Recall the basic albeit tedious Node server we set up that handled basic routing:
// nodeServerTwo.js
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
if (req.url === '/') { // user wants the home page
res.writeHead(200, { 'content-type': 'text/html' });
const homePageHTML = fs.readFileSync('node.html');
res.write(homePageHTML);
res.end();
} else if (req.url === "/node.png") {
res.writeHead(200, { 'content-type': 'image/png' });
const image = fs.readFileSync('./node.png');
res.write(image);
res.end();
} else if (req.url === "/styles.css") {
res.writeHead(200, { 'content-type': 'text/css' });
const css = fs.readFileSync('./styles.css');
res.write(css);
res.end();
} else {
res.writeHead(404, { 'content-type': 'text/html' });
res.write(`<h4>Sorry, this isn't the page you're looking for!</h4>`)
res.end()
}
});
server.listen(3000);
Using the routing concepts discussed in the previous note, we will now try to recreate the plain Node server above but in Express.
For us to accomplish this, we will start by noting that app
comes with a use
method. Per the docs, the app.use([path,] callback [, callback...])
syntax results in mounting the specified middleware function or functions at the specified path: the middleware function is executed when the base of the requested path matches path
.
In our specific use case, we will not explicitly provide the path or callback but instead pass a built-in middleware function in Express, namely express.static('public')
. As noted in the docs for express.static(root, [options])
: This is a built-in middleware function in Express. It serves static files and is based on serve-static
. The root
argument specifies the root directory from which to serve static assets. The function determines the file to serve by combining req.url
with the provided root directory. When a file is not found, instead of sending a 404 response, it instead calls next()
to move on to the next middleware, allowing for stacking and fall-backs. (See the rest of the docs for more details as well as adding options to express.static
.)
Worth noting is how express.static
actually works in light of how most Express applications begin:
const express = require('express');
const app = express();
We noted previously how the createApplication
function is the default export from the express
module. But there are several other exports, one of which is the static
method we are now going to use. If we look in the node_modules
folder as we did before, then we will see the following lines among others:
/**
* Expose middleware
*/
exports.json = bodyParser.json
exports.query = require('./middleware/query');
exports.raw = bodyParser.raw
exports.static = require('serve-static');
exports.text = bodyParser.text
exports.urlencoded = bodyParser.urlencoded
Right now, of course, we are interested in the exports.static = require('serve-static');
line. We can see how express.static
is based on the serve-static
module. We could inspect the serve-static
module and see what the default export is (it's the function serveStatic
), but we will not go into the details here. The important thing is that we can pass this function a directory name, say public
as is often the case, and anytime anybody wants to see a resource located in public
, we do not have to worry about routing or anything like that.
NOTE: How express.static works (and generally serving up numerous files) |
---|
Some people actually use express.static to serve up entire front-end sites. If using express.static('public') , you could drop an entire front-end site into the public folder and you're done. You don't have to deal with many headaches you might have to otherwise endure. You do not (and should not ... it will not work if you do ... you will get a 404 error) put public in front of the path to the resource you want to access that is being statically served. For example, if node.png is in the public folder and we are listening on port 3000, then we can access this picture by going to localhost:3000/node.png instead of localhost:3000/public/node.png .For the sake of clarity, although you would never do this in practice, you could also have app.use(express.static('node_modules')) and then you'd be statically serving up all the files in the node_modules folder. And then we could access whichever one we want, say the HISTORY.md one in the accepts node module, like so: localhost:3000/accepts/HISTORY.md .The point is that if we execute app.use(express.static('folder-name')) then the server knows that everything in folder-name is going to be served up as part of the root domain. It's worth noting too that you can have as many app.use(express.static('folder-name')) commands as you want because they're being attached to the entire app lication which is being served up on port 3000; that is, you can statically serve the contents of as many folders as you want. Do take care, however, that you do not statically serve something that should not be readily accessible. This is why the convention is to name the folder public whose contents you want to be made publicly available. Typically, the stuff you would want to be statically served are things like stylesheets, images, etc. |
To fully recreate our Node server, we will not use Node to read in our node.html
file with the fs
module but instead use the sendFile
method on the response
object to achieve the same thing (courtesy of the native path
module so we can use an absolute path which is required). That is, instead of having to deal with
if (req.url === '/') {
res.writeHead(200, { 'content-type': 'text/html' });
const homePageHTML = fs.readFileSync('node.html');
res.write(homePageHTML);
res.end();
}
we will simply have something like the following:
app.all('/', (req, res) => {
res.sendFile(path.join(__dirname, 'node.html'));
})
It's very uncommon to do something like res.sendFile
; instead, typically you would do something like res.render
, but we have not gotten to templates yet which will make that feasible.
NOTE: How __dirname works |
---|
As noted on a Stack Overflow thread, __dirname is only defined in scripts (i.e., .js files). It's not available in the Node REPL. Basically, __dirname means "the directory of this script." In REPL, you do not have a script. Hence, __dirname would not have any real meaning. It's too bad this is the case because loading a script file while inside the REPL using .load will result in an error if you used __dirname in your script. One way to get around this is, inside of the REPL, do something like __dirname = process.cwd() . In fact, in light of this, instead of using path.join(__dirname, 'node.html') as we did above, we could just as well use process.cwd() + '/node.html' as the argument to res.sendFile . There's a lot more about process and its available methods in the docs. Always look at the docs. |
Recapping, we have the following two equivalent ways of handling the basic routes and serving up static files:
Using Express:
// expressServerTwo.js
const path = require('path');
const express = require('express');
const app = express();
app.use(express.static('public'));
app.all('/', (req, res) => {
res.sendFile(path.join(__dirname, 'node.html'));
})
app.all('*', (req,res) => {
res.send(`<h1>Sorry, this page does not exist</h1>`)
})
app.listen(3000);
Using plain Node:
// nodeServerTwo.js
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
if (req.url === '/') { // user wants the home page
res.writeHead(200, { 'content-type': 'text/html' });
const homePageHTML = fs.readFileSync('node.html');
res.write(homePageHTML);
res.end();
} else if (req.url === "/node.png") {
res.writeHead(200, { 'content-type': 'image/png' });
const image = fs.readFileSync('./node.png');
res.write(image);
res.end();
} else if (req.url === "/styles.css") {
res.writeHead(200, { 'content-type': 'text/css' });
const css = fs.readFileSync('./styles.css');
res.write(css);
res.end();
} else {
res.writeHead(404, { 'content-type': 'text/html' });
res.write(`<h4>Sorry, this isn't the page you're looking for!</h4>`)
res.end()
}
});
server.listen(3000);
Hopefully it is clear just how much nicer Express is to work with and how much easier we could get things to scale if we wanted or needed to.
Middleware Basics
Middleware in Express: general overview and examples
Express claims itself to really be two things:
- A router. We saw several of the possibilities previously with stuff like
app.post
,app.get
, etc. - A series of middleware that comprises a web framework.
What does the second point really mean? Middleware is something that happens ... in the middle ... of something. What do we do a lot of in web applications? We get requests and send responses. Middleware is stuff we can do between getting the request and sending back the response: req ---MIDDLEWARE---> res
. In more "Express-ish" terms, a middleware function is ANY function that has access to the req
, res
, and next
objects. That said, basically Express is just a bunch of middleware! It's a whole bunch of little functions working in unison that have access to req
, res
, and next
that slowly piece together a cool web framework (that and a router).
Maybe think of an illustrative example like this:
- Request comes in
- We need to validate the user (sometimes)
- We need to store some things in the database
- If there is data from the user, then we need to parse it and store it
- Respond
Steps 2-4 above are all situations that must be addressed with middleware functions. They are everything that happens between getting the request and firing back a response.
The response always depends on the request. The nature of the dependence (i.e., large or small) could be negligible as we have seen with what we have done so far (i.e., basically sending back some stuff regardless of what kind of request we receive). But in some cases it could matter quite a bit (e.g., whether or not a user is validated). The point is that how the response is constructed always depends on the request in some manner, and Express has a locals
property on every response
object intended to effectively capture whatever we want from a specific request--what we capture from the specific incoming request
can be stored as local variables on the response
object via res.locals
.
As the docs communicate about res.locals
: An object that contains response local variables scoped to the request, and therefore available only to the view(s) rendered during that request/response cycle (if any). Otherwise, this property is identical to app.locals
. This property is useful for exposing request-level information such as the request path name, authenticated user, user settings, and so on:
// Documentation example usage of how you might want to use res.locals
app.use(function (req, res, next) {
res.locals.user = req.user
res.locals.authenticated = !req.user.anonymous
next()
})
Basically, the response
object has a property called locals
that is pre-built into Express--it is attached to every response, and it will live for the life of the response and it's very useful for passing data over to a template. For now, it is simply nice to know that we will be able to pass res.locals
around from place to place. Every middleware function will have access to res.locals
for the life of the response. How? Because every middleware function has access to the response
object.
We can illustrate all of this by means of a somewhat phony example involving some validateUser
middleware:
const express = require('express');
const app = express();
function validateUser(req, res, next) {
res.locals.validated = true;
next();
}
// app.use(validateUser); // <-- COMMENTED OUT ON PURPOSE
app.use('/admin', validateUser);
app.get('/', validateUser);
app.get('/', (req, res, next) => {
res.send(`<h1>Main Page</h1>`)
console.log(`Validated? ${!!res.locals.validated}`);
})
app.get('/admin', (req, res, next) => {
res.send(`<h1>Admin Page</h1>`)
console.log(`Validated? ${!!res.locals.validated}`);
})
app.get('/secret', (req, res, next) => {
res.send(`<h1>This is a secret page. Go away!</h1>`);
console.log(`Validated? ${!!res.locals.validated}`);
})
app.use('*', (req, res) => {
res.send(`<h1>Woops! Is no good.</h1>`);
console.log(`Validated? ${!!res.locals.validated}`);
})
app.listen(3000);
Let's now unpack some of the stuff from above:
Artificial use of res.locals
in validateUser
As can be seen with our definition of validateUser
and our use of res.locals
, no reference is even made to the request
object. Nearly always the local variables you want on the response
object will depend, in some way, on the request
object as the example in the docs shows:
app.use(function (req, res, next) {
res.locals.user = req.user
res.locals.authenticated = !req.user.anonymous
next()
})
We will get to such common use cases very soon.
Non-anonymous middleware by defining validateUser
globally
The validateUser
function seen above is an explicit, named function declaration which is different than much of the other middleware we have used to this point. Since validateUser
is a function whose signature contains req
, res
, and next
, we should note that something like
app.get('/', validateUser);
is basically equivalent to
app.get('/', (req, res, next) => {
res.locals.validated = true;
next();
})
The main difference is that declaring the validateUser
middleware globally will give us access to it globally; that is, we can use validateUser
wherever we want whereas the callback function/middleware in
app.get('/', (req, res, next) => {
res.locals.validated = true;
next();
})
is only available when a GET
request is made to the root.
Effect of next()
It is hard to overstate how critical next()
really is. Suppose we omitted it in our definition of validateUser
:
...
function validateUser(req, res, next) {
res.locals.validated = true;
// next();
}
app.use(validateUser);
...
What would happen here? Any route that expected us to use (i.e., invoke) the validateUser
function would not actually get around to sending a response. If we actually ran app.use(validateUser);
, then validateUser
would be invoked for whatever path we could try to reach, and doing so would result in validateUser
running but us never actually sending a response back. The browser would just hang. Not good! Don't forget next()
: you want to hand control off to the next piece of middleware in the cycle (probably the actual routing you have set up).
app.use(validateUser);
The reason app.use(validateUser);
is commented out is exactly because of its effect: it results in invoking validateUser
every time any HTTP request is made to any path (i.e., validateUser
is used at the "application level"). Of course, in some cases you may want to do this. But the use cases are likely few.
app.use('/admin', validateUser);
The effect of app.use('/admin', validateUser);
is that we are telling Express to use validateUser
for any type of HTTP request to only the /admin
path.
app.get('/', validateUser);
The effect of app.get('/', validateUser);
is that we are telling Express to use validateUser
only on GET
requests for only the path /
.
Third-party security middleware: helmet
As noted in the Express docs on security best practices: "Helmet can help protect your app from some well-known web vulnerabilities by setting HTTP headers appropriately. Helmet is actually just a collection of smaller middleware functions that set security-related HTTP response headers: [...]."
To use the middleware offered by helmet
, it's as simple as app.use(helmet());
. You do, however, need to be somewhat mindful in terms of what major version of helmet
you use (now 4+; the version history indicates the major version was updated from 3 to 4 in July of 2020).
The simple story:
app.use(helmet());
in major version 3 will probably not cause issues (7 of 11 middleware functions are used by default), but using helmet
in that way in major version 4 could cause issues (all 11 middleware functions are used by default), specifically errors related to not being able to load resources (stylesheets, scripts, etc.): "Refused to load the <resource>
... because it violates the following Content Security Policy directive ...":
The simple solution now is to use
app.use(helmet({ contentSecurityPolicy: false }));
instead of just app.use(helmet())
(all 11 middleware functions will be used except the sometimes problematic contentSecurityPolicy
one). See below for more details.
Helmet: Major Version 3
For major version 3 of helmet
, we saw this in the documentation:
Helmet is a collection of 11 smaller middleware functions that set HTTP response headers. Running app.use(helmet())
will not include all of these middleware functions by default.
Module | Default? |
---|---|
contentSecurityPolicy for setting Content Security Policy | |
crossdomain for handling Adobe products' crossdomain requests | |
dnsPrefetchControl controls browser DNS prefetching | ✔ |
expectCt for handling Certificate Transparency | |
frameguard to prevent clickjacking | ✔ |
hidePoweredBy to remove the X-Powered-By header | ✔ |
hsts for HTTP Strict Transport Security | ✔ |
ieNoOpen sets X-Download-Options for IE8+ | ✔ |
noSniff to keep clients from sniffing the MIME type | ✔ |
referrerPolicy to hide the Referer header | |
xssFilter adds some small XSS protections | ✔ |
In the table above, take note that the contentSecurityPolicy
middleware is not loaded by default.
Helmet: Major Version 4
As noted in the docs for the current version of helmet
:
The top-level helmet function is a wrapper around 11 smaller middlewares. In other words, these two things are equivalent:
// This...
app.use(helmet());
// ...is equivalent to this:
app.use(helmet.contentSecurityPolicy());
app.use(helmet.dnsPrefetchControl());
app.use(helmet.expectCt());
app.use(helmet.frameguard());
app.use(helmet.hidePoweredBy());
app.use(helmet.hsts());
app.use(helmet.ieNoOpen());
app.use(helmet.noSniff());
app.use(helmet.permittedCrossDomainPolicies());
app.use(helmet.referrerPolicy());
app.use(helmet.xssFilter());
To set custom options for one of the middleware, add options like this:
// This sets custom options for the `referrerPolicy` middleware.
app.use(
helmet({
referrerPolicy: { policy: "no-referrer" },
})
);
You can also disable a middleware:
// This disables the `contentSecurityPolicy` middleware but keeps the rest.
app.use(
helmet({
contentSecurityPolicy: false,
})
);
Major Changes with Helmet from v3 to v4
The changelog for helmet
details what all has changed, but the Helmet 4 upgrade guide is really the useful source of information, specifically the section on which middlewares were added by default:
When you use the top-level Helmet function (i.e., app.use(helmet())
), Helmet 4 now includes the following middlewares by default:
helmet.contentSecurityPolicy
(which sets theContent-Security-Policy
header; see below)helmet.expectCt
(which sets theExpect-CT
header)helmet.permittedCrossDomainPolicies
(which sets theX-Permitted-Cross-Domain-Policies
header)helmet.referrerPolicy
(which sets theReferrer-Policy
header)
These were present in Helmet 3 but were disabled by default.
The section immediately below this in the guide details what changed in the Content-Security-Policy
middleware: The Content-Security-Policy
middleware had the biggest changes.
There is now a default policy. Helmet 3 disabled CSP by default. Helmet 4 does not, and sets one. If this is causing problems, you can disable the CSP header:
app.use(helmet({ contentSecurityPolicy: false }));
Alternatively, for more security, you can craft a Content Security Policy for your site.
Built-in middleware: express.json
and express.urlencoded
In this note we will talk about two important methods that belong to the express
module that have not been used yet, namely express.json
and express.urlencoded
, and then one other piece of middleware (from the helmet
module) that is not native to Express but that we really should always use for security reasons (always wear your helmet
!).
Looking at the docs, we see a few Express methods we can use:
express.json()
express.raw()
express.Router()
express.static()
express.text()
express.urlencoded()
We have already touched on express.static
, but we want to look at express.json
and express.urlencoded
now. We'll look at the others later. Let's start with express.json
.
As the docs note, express.json([options])
is a built-in middleware function in Express. It parses incoming requests with JSON payloads and is based on body-parser
. Returns middleware that only parses JSON and only looks at requests where the Content-Type
header matches the type
option (the default type
option is application/json
... see the link for all options and defaults). This parser accepts any Unicode encoding of the body and supports automatic inflation of gzip
and deflate
encodings. A new body
object containing the parsed data is populated on the request
object after the middleware (i.e., req.body
), or an empty object ({}
) if there was no body to parse, the Content-Type
was not matched, or an error occurred.
Read the above excerpt from the docs again (and actually visit the docs for all sorts of good and more detailed information). Since express.json
is based on body-parser
, just like express.static
is based on serve-static
, we can see that by installing the express
module we also install the body-parser
module as a dependency (we cannot use express.json
without body-parser
and express.json
is built-in middleware in Express), we can take a look inside the express
node module and we will find body-parser
listed as one of the dependencies
in the package.json
. So what does this mean? Well, if someone sends you JSON, meaning the Content-Type
is going to come through as application/json
or something along those lines, then express.json
will kick into action and parse the body for us. Note that any data that comes into any server (via form submission or whatever), even if it's an Express server, is still going to be interpreted as a basic string. It doesn't make any difference. That's how servers work. The string needs to be parsed to be of much use and we can do that thanks to express.json
. Of course, we will want to use this everywhere in our Express application (not just on select paths or routes) and thus we will use it like so:
app.use(express.json())
Now let's consider express.urlencoded([options])
. As the docs note, this is a built-in middleware function in Express. It parses incoming requests with urlencoded payloads and is based on body-parser. Returns middleware that only parses urlencoded bodies and only looks at requests where the Content-Type
header matches the type
option (the default type
option is application/x-www-form-urlencoded
... see the link for all options and defaults). This parser accepts only UTF-8 encoding of the body and supports automatic inflation of gzip
and deflate
encodings. A new body
object containing the parsed data is populated on the request
object after the middleware (i.e., req.body
), or an empty object ({}
) if there was no body to parse, the Content-Type
was not matched, or an error occurred. This object will contain key-value pairs, where the value can be a string or array (when extended
is false
), or any type (when extended
is true
).
We will want to use the express.urlencoded([options])
middleware at the application level just like express.json([options])
and it's typically good to set the extended
property to false
(the default is true
) to ensure each value in the key-value pairs that make up the body
of the request
(i.e., req.body
) will be a string or an array:
app.use(express.urlencoded({extended: false}))
See this Stack Overflow post about using application/x-www-form-urlencoded
as opposed to multipart/form-data
when post
ing data using HTTP (typical when posting data via forms, for instance). As the top answer on the SO thread notes: "For application/x-www-form-urlencoded
, the body of the HTTP message sent to the server is essentially one giant query string -- name/value pairs are separated by the ampersand (&
), and names are separated from values by the equals symbol (=
). An example of this would be: MyVariableOne=ValueOne&MyVariableTwo=ValueTwo
."
Example of express.json
and express.urlencoded
middleware in action by making an AJAX post request
To make much of what appeared in the previous note more concrete, consider the following setup: in the root of the project folder, make a public
folder and create an ajax.html
file with the following contents:
<!-- ajax.html -->
<h1>AJAX test page has been loaded!</h1>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
function makeOurRequest() {
return $.ajax({
method: "POST",
url: "http://localhost:3000/ajax",
dataType: "text",
// dataType: "json",
data: {
name: "Daniel"
}
});
}
const theRequest = makeOurRequest();
theRequest
.then(response => console.log(`AJAX request successful! Response: `, JSON.parse(JSON.stringify(response))))
.catch(err => console.log(`AJAX request failed! Error: `, err))
</script>
Then at the root of the project folder create a server.js
file:
const express = require('express');
const app = express();
const helmet = require('helmet');
app.use(helmet());
app.use(express.static('public'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.post('/ajax', (req, res) => {
console.log(req)
// console.log(req.headers)
res.send('THIS IS A TEST RESPONSE AS PLAIN TEXT') // for dataType: 'text' in ajax request
// res.send({message: 'this is a test response as JSON'}) // for dataType: 'json' in ajax request
});
app.listen(3000);
Let's walk through what happens when you visit http://localhost:3000/ajax.html
:
- First, we can only access the
ajax.html
file because it is being statically served thanks toapp.use(express.static('public'))
. Once the document is served and loaded, the scripts in the file fire: jQuery is made available to us and then we make an AJAX request with the following properties:method: 'POST'
: We are making a post request (which will almost always be the case with form submissions and the like).url: "http://localhost:3000/ajax"
: We are making a POST request to the/ajax
route--Express will be responsible for accepting the request and putting together a response.dataType: 'text'
ordataType: 'json'
: What kind of data are we sending through?data: { name: 'Daniel' }
: The actual data we are sending through.- Finally, once the request has been made, the return value from the AJAX request is a promise. If that promise is resolved or rejected, then we will respond to indicate so and this can be seen in the browser console.
- In
server.js
, we have constructed things so that when apost
request is made to theajax
route, we first log the request in the Node console (and later the request headers specifically) withconsole.log(req)
(which will become relevant soon) and then we send our response (which we have set up to be either text or JSON).
Note about dataType and arg in res.send(arg) |
---|
The data type of what we send back in our response should match the dataType of the AJAX request. If we specify dataType: 'json' in the AJAX request but send back text (e.g., 'THIS IS A TEST RESPONSE AS PLAIN TEXT' ) as our response, then the returned promise from the AJAX request will be rejected. On the other hand, if we specify dataType: 'text' on the AJAX request but send back JSON (e.g., {message: 'this is a test response as JSON'} ), then what we actually get back will be the object as text which is not what we want: {"message":"this is a test response as JSON"} instead of {message: "this is a test response as JSON"} . |
The above note touches on making sure what is being sent and requested are as expected, but what we really do not want to happen is to leave off app.use(express.json());
or app.use(express.urlencoded({extended: false}));
in our server.js
file. Why? Run the server and keep an eye on the console (not the browser console but the console in Node) for what shows up when you visit http://localhost:3000/ajax.html
. With everything as it is currently, you should see something like the following towards the end of the logged request object:
...
body: [Object: null prototype] { name: 'Daniel' },
...
What does this mean? It means the data
sent through (i.e., POST
ed) by the AJAX request is coming through as a value (notably as JSON) on the body
property for the incoming req
uest object. This is great! What good would form submissions and stuff of that nature be if you could not actually pull the data from the form submissions effectively? That is what app.use(express.json());
and app.use(express.urlencoded({extended: false}));
allow us to do.
Try repeating the above process but this time comment out app.use(express.urlencoded({extended: false}));
. What do you see logged to the console for the body
object? You probably see a line like the following:
...
body: {},
...
Now repeat the process yet again but this time also commenting out app.use(express.json());
. What do you get for the body
object? Nothing at all! That's not good. Now, you could probably try to parse things yourself and find the data you want somewhere in the req
object, but why do this when all of it has been made easier for you? Express middleware to the rescue!
One point of curiosity: Why did we get the data we wanted when we used app.use(express.urlencoded({extended: false}));
but only got {}
when it was commented out? This has to do with the headers. Visit the page again without commenting out app.use(express.json());
or app.use(express.urlencoded({extended: false}));
while also changing console.log(req)
to just console.log(req.headers)
. You should then see a line like the following:
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
From above, we can see what the mime-type is: application/x-www-form-urlencoded
. Now that is why we need the app.use(express.urlencoded({extended: false}));
middleware. When a form comes through, unless specified otherwise, usually the default is going to be application/x-www-form-urlencoded
for the content-type
. That's typically how data is passed around by default. Oftentimes it will also be passed around as application/json
or text/json
. That's what app.use(express.json());
is for. But we need urlencoded
because if someone sends us data with the header shown above, then we need some middleware to parse it and the parsing result is made available to us in the body
object. The data is stored on the body
property of the req
object likely because the middleware is based on body-parser
.
From everything we have seen so far, it is safe to say good practice is to basically always include the following on any Express application:
app.use(helmet());
app.use(express.static('public'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
It will cover most of your bases and save you from a bunch of headaches of why something doesn't work, why your data isn't coming through, etc.
One last thing to note concerns the use of helmet
middleware. It sets HTTP headers right upfront and protects you from a bunch of well-known vulnerabilitie. There's really no reason not to use this middleware. It's very simple:
npm i helment // install helmet in your package.json
const helmet = require('helmet'); // import its contents (check npm for more options)
app.use(helmet()); // use it at the application level
Remember: When using Express, make sure to use your helmet
.
Using res.json()
to respond with JSON
In the previous note, in one of the examples, we manually tried to respond with JSON by including res.send({message: 'this is a test response as JSON'})
within Express and having dataType: 'json'
in our AJAX request. But we can do much better. Looking at the docs in the section about the response
object, we see res.json([body])
. This seems to be what we want.
From the docs: res.json([body])
sends a JSON response. This method sends a response (with the correct content-type
) that is the parameter converted to a JSON string using JSON.stringify. The parameter can be any JSON type, including object, array, string, Boolean, number, or null, and you can also use it to convert other values to JSON.
With this new method, we can drop the dataType
on the AJAX request completely and just use res.json
:
res.json('Some text');
res.json({message: 'an object without setting dataType on AJAX request'});
res.json(['Some text', {message: 'a message'}, [1,2,3], 4]);
As the docs note, any one of the above attempts to respond would result in the parameter (whether it be simple text, an object, an array of a bunch of other things, etc.) being converted to a JSON string using JSON.stringify
before being sent over the server (remember everything sent over the server is done so as a string ... we simply want the string to be a JSON string which can be effectively parsed).
To recap: By default, res.send
is going to set a mime-type of content-type: text/html
. If, however, we use res.json
, then the mime-type will be set as content-type: application/json
. The takeaway is that res.json
is incredibly important because anytime you need to respond with JSON, which will be very often depending on what you're building, then you're going to use res.json
and not res.send
. Any time you are going to respond with HTML, then you will probably use res.render
, something we will get to shortly.
Rendering Basics
Express usage: API development and/or server-side rendering (res.json
and/or res.render
, respectively)
Something to note which will become more and more evident as you build with Express: res.render
and res.json
loosely represent the two main things you would typically do with Express. To make the upcoming need/use of res.render
evident, let's consider a scenario back in the day where say you wanted to visit MySpace before Facebook killed everything.
You would get on your computer and go to www.myspace.com
and you'd connect with their servers. You'd send in a request and the server would need to kick back a response to your browser (which basically only understands HTML, CSS, and JavaScript). So the question becomes: What goes on inside of the server when your request is received? What happens inside? It may be helpful to consider what full stack development is really all about and to look at what makes up a server:
You have a number of different layers in the stack. From bottom to top:
- OS (operating system): The server is a computer and you have to have an operating system. You can't do anything without an operating system because you need some means of your software interacting with the hardware. Typically the OS will be Linux, Windows, UNIX, etc.
- WS (web server): Apache, Nginx, IIS, etc.
- DB (database layer): You'd have a whole host of SQL and NoSQL options: MySQL, PostgreSQL, Oracle, MongoDB, Apache CouchDB, etc.
- PL (programming layer): C, C++, Java, Python, Ruby, PHP, R, etc.
- Front-end UI (bonus): Not really a part of the server stack but part of full stack development (the front-end layer). You have React, Vue, Angular, etc.
The OS, WS, DB, and PL layers make up the server. When a user sends a request to port 80 (a port created by the transport layer), what happens? How does the server decide how to respond with HTML, CSS, and JavaScript? Since everyone on MySpace has their own page (just like Facebook), the question becomes: Does MySpace have individual HTML pages stored on the hard drive for every single user (millions of HTML pages, all very similar) that they serve up? Of course not.
What happens is that the user gets to port 80, and then the web server kicks into gear (say it's Apache). Suppose it's running PHP. It starts processing and interpreting PHP. It realizes it needs some stuff from the database (say from MySQL), go back to running some PHP, get some more stuff from the database (hopping back and forth, back and forth, ...). Eventually the back and forth process finishes, and Apache has finished all of its processing. It's read everything, and Apache sends it back out the door. At this point, then, PHP and MySQL have worked together to create HTML, CSS, and JavaScript. What it created was handed off to Apache/Nginx/IIS and that was sent back across the wire via HTTP.
That's how it happens. There's not an individual HTML file that is requested and then sent back. A whole bunch of stuff goes on inside of the server to ensure all of the proper information is being grabbed. A specific HTML file for you does not exist. What likely exists is some sort of template file. Every user page, no matter how complicated, appears basically the same. Maybe colors are different, the song was different, etc. But the structural integrity (think HTML) of each user page was the same. But all of the particulars about different parts of the structure depended on user-supplied information (stored in databases).
The key here is that the initial request goes out and grabs the HTML, CSS, and JavaScript once it's been made available. But the servers at MySpace are going to have to prepare a new response for every new request. Each new request is like starting over from scratch. So every time we go to our user page, the user page is built up and sent back. Built up and sent back. Every single time. This is called server-side rendering. Because the server is in charge of putting together the HTML, CSS, and JavaScript and sending it back to the browser. Wikipedia still does this. Each page you go to you've restarted the whole process from scratch. That is what we will be doing with res.render
. So within Express, we will be able to create a template, however complicated. And the server is going to process our template into HTML, CSS, and JavaScript. And the template will ultimately be replaced by user-specific information retrieved in a variety of ways. That is server-side rendering. The server is in charge of everything on every page load always.
The other weapon is res.json
. And what would happen in a more modern sense is you would go out to a place like Facebook and the first time you go out you have to get everything. You have to get all the necessary HTML, CSS, and JavaScript. Every following request, every time you click on something after that, because it is a single page application in the case of something like React, when you make a new request, instead of making a full-blown request, you are going to use AJAX and, instead of sending back HTML, CSS, and JavaScript, the server is just going to send back JSON. And that original HTML document which was loaded upon the first request, the JSON is going to go there and update the DOM. So it will look like a new page, but it's really just the same HTML, CSS, and JavaScript, but it's going to have some new data in it thanks to the JSON. Since we're using AJAX, we will still have a new request. We will still have a req
and a res
, it will still be our responsibility to handle that network traffic, but instead of responding with a template (with all the HTML, CSS, and JavaScript), we are just going to respond with JSON.
So the quick review: res.render
is server-side rendering whereas res.json
is going to mostly be for API/JSON needs. Server-side rendering is, "I am going to the server and every time the server is going to respond with new, fresh HTML, CSS, and JavaScript. Every single time. Always. Think Wikipedia. With res.json
, or an API type situation, you as the user are going to go out, hit the server, and the server the first time is going to send you a whole bunch of HTML, CSS, and JavaScript. But every time after that, the server is just going to send JSON. And the page or the DOM will update itself accordingly to reflect the incoming data (i.e., JSON) after an AJAX request. Think Facebook or Amazon.
In one case, you always have to come back to the server. The nice thing with res.render
is you can make use of session variables, cookies, etc. The user always has to come to the server for everything because the server contains everything. The other architecture is very fast, it creates a great UI/UX opportunity, but you have to start storing stuff on the browser you would maybe not normally want to store there.
View Engines
Making server-side rendering a reality
Let's wire up a basic Express server as we have done previously:
const express = require('express');
const app = express();
const helmet = require('helmet');
app.use(helmet());
app.use(express.static('public'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.get('/', (req, res, next) => {
// res.send('Sanity check')
// res.json({msg: 'Success!'}) // Send back some legitimate JSON
res.render('index') // We get an error without a view engine
})
app.listen(3000)
Right now, if we visit http://localhost:3000/
, we'll get an error:
Error: No default engine was specified and no extension was provided.
The error message you might get sometimes can be rather scary, but often a good strategy is to look for whatever files you have created. If we do that, then we will see (in my case)
at /Users/danielfarlow/[...]/just-express/express201/rendering.js:14:7
This is telling us that there is an issue in the rendering.js
file (which we have created) on line 14, character 7. In order to use res.render
, the process goes something like this:
- Typical Express build/app template: Express as we know it "happens" (i.e., our
rendering.js
file is nothing special except for trying to useres.render
in one of its route handling response methods; otherwise it's business as usual). All the Node happens. We include express (i.e.,const express = require('express')
), we make our app (i.e.,const app = express()
), we do our middleware, we build our routes, etc. - Specify view enginer: We define a view engine. There are several options (these are just a few of the most popular ones):
- EJS (Embedded JavaScript): From the home page: What is the "E" for? "Embedded?" Could be. How about "Effective," "Elegant," or just "Easy"? EJS is a simple templating language that lets you generate HTML markup with plain JavaScript. No religiousness about how to organize things. No reinvention of iteration and control-flow. It's just plain JavaScript. Install via NPM with
npm install ejs
. - Mustache: Logic-less templates for a variety of templates. If we were to use this, then we would be interested in mustache.js, a zero-dependency implementation of the mustache template system in JavaScript. Install via NPM with
npm install mustache
. - Handlebars: Minimal templating on steroids. Handlebars.js is an extension to the Mustache templating language created by Chris Wanstrath. Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be. Install via NPM with
npm install handlebars
. - Jade/Pug: Pug is a high-performance template engine heavily influenced by Haml and implemented with JavaScript for Node.js and browsers. Previously named "Jade," it is now named "Pug" thanks to the fact that "Jade" was a registered trademark. Basically, Pug is a clean, whitespace sensitive syntax for writing HTML (see the Pug GitHub page or its API reference for more). Install via NPM with
npm install pug
.
- Employ
res.render
: Inside one of our routes we have ares.render
. - Give
res.render
a file and data: We pass thatres.render
two things: 1. The file we want to use (e.g., an.ejs
file, a.mustache
file, a.handlebars
or.hbs
file, a.pug
file, etc.). 2. The data we want to send to that file. - Combine Node code with HTML/CSS/JS via view engine: Express uses the node module for our specified view engine and parses the file accordingly. That means it takes the HTML, CSS, and JavaScript and combines it with whatever "node" there is in the file (i.e., the data available in
res.locals
). - Finished product (just HTML/CSS/JS): The final result of this process is a compiled product of the things the browser can read (i.e., HTML, CSS, and JavaScript).
All the steps above constitute "the round trip" for a res.render
. The templating engine serves as a bridge between Node and the front-end stuff. We can make a template out of HTML, CSS, and JS, and we can have a bridge that will allow us to access Node.js stuff. The specific bridge is the second argument to res.render
(i.e., the data we want to send to our template file that Node has access to). That object, the data we want to send to our template file, is made available as res.locals
. It will give us the ability to pass in a user's name, whether or not the user is validated, and generally any kind of data we might want to send over to the template. And then the template engine can fill out the HTML accodingly with the given data. So Express uses the node module for the view engine and will parse the template file out. It will combine all of the "node stuff" (i.e., the data we make available in res.locals
) and combine it with HTML, CSS, and JavaScript to return a product of only HTML, CSS, and JavaScript that can be sent to the requesting client.
Before we can effectively use res.render
, we need to use app.set
to tell Express what will be used as the templating engine. Note that app.set
is used for more than just this functionality though.
Note from The docs on app.set(name, value) |
---|
app.set(name, value) assigns setting name to value . You may store any value that you want, but certain names (like 'view engine' in our case) can be used to configure the behavior of the server. These special names are listed in the app settings table.Calling app.set('foo', true) for a Boolean property is the same as calling app.enable('foo') . Similarly, calling app.set('foo', false) for a Boolean property is the same as calling app.disable('foo') . Retrieve the value of a setting with app.get() : app.set('title', 'My Site') and then app.get('title') // "My Site" |
Underneath the high-level description above in the docs, we see a section about "Application Settings" that provides a table of different properties (where each property corresponds to a name
, the first argument to app.set
), the type for that property, a description of the property, and what the default value is. In particular, we see view engine
is one such property, it's a string, a description (the default engine extension, file name extension that is, to use when omitted; note: sub-apps will inherit the value of this setting), and finally a default value of undefined
(we will need to provide a value that reflects the template or view engine we want to use whether that's ejs
, hbs
, pug
, etc.).
With all of the above said, let's modify our server by running npm install ejs
and then add app.set('view engine', 'ejs')
and see if we encounter any errors:
const express = require('express');
const app = express();
const helmet = require('helmet');
app.use(helmet());
app.use(express.static('public'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.set('view engine', 'ejs');
app.get('/', (req, res, next) => {
res.render('index');
})
app.listen(3000)
If we go to http://localhost:3000/
, then we do get an error, namely the following:
Error: Failed to lookup view "index" in views directory "/Users/danielfarlow/[...]/express201/views"
So what happened? We failed to lookup view 'index'
. Why index
? Well, we told Express to go looking for a file index
by using res.render('index')
. What should the file extension of index
be? It should be .ejs
according to app.set('view engine', 'ejs')
. Well, we don't have an index.ejs
file. Furthermore, Express went looking for the index.ejs
file in the views
folder. But we don't currently have a views
folder. Why did Express go looking for this file in the views
folder?
The answer, as usual, is in the documentation. In the "Application Settings" table under the docs entry for app.set
we see a name
of views
that is expected to be a string or array, with a description (a directory or an array of directories for the application's views. If an array, the views are looked up in the order they occur in the array), and finally a default value of process.cwd() + '/views'
(i.e., the current working directory or cwd
with /views
appended to it; basically, it's just looking for the views
folder in your project directory). We're going to be more explicit than process.cwd() + '/views'
by using the path
module and using path.join(__dirname, 'views')
. What's the difference?
Note: process.cwd() vs __dirname in Node.js |
---|
Some helpful comments can be found on a Stack Overflow post. Basically, the cwd in process.cwd() is a method of the global object process where the return value is a string representing the current working directory of the Node.js process (i.e., where you are currently running Node or simply the directory from which you invoked the node command). On the other hand, __dirname is the string value of the directory name for the directory containing the current script. The big difference is that __dirname is not actually a global but rather local to each module. You can always execute process.cwd() to find out where the Node.js process originated or is running (you can actually change this with process.chdir but we do not need to worry about that right now).In a nutshell, knowing the scope of each makes things easier to remember. process is node 's global object, where .cwd() returns where Node is running. __dirname is module 's property, where the value represents the file path of the module. Similarly, __filename is another module 's property which holds the file name of the module. |
All that said, require
the native path
module at the top of your server like so: const path = require('path');
. Then, underneath app.set('view engine', 'ejs');
we can add app.set('views', path.join(__dirname, 'views'));
. Then create a views
folder and place an index.ejs
file inside of the views
folder with just a <h1>Rendered file!</h1>
for right now. So here are the three pieces to res.render
for a specific file:
- The file name: For example:
index
inres.render('index');
- The type or extension of the file: For example:
app.set('view engine', 'ejs');
. This tells us we will be looking forindex.ejs
. - The location of the file: For example:
app.set('views', path.join(__dirname, 'views'));
tells us the file will be in theviews
directory which should be at the same level at whatever script we are writing our code in (see note above). If we have more than one folder for the views, then the docs note we can include something likeapp.set('views', [folder1, folder2, ...]);
where each folder will be searched for the file until the first one is found. It should be noted here that Express will not search subdirectories you create within theviews
directory if you plan on creating subdirectories.
Using more than one view engine
In the note above, we learned the basics of wiring up Express with a view engine. It's entirely plausible (albeit somewhat unlikely) that you would want to use more than one view engine. Maybe you liks EJS for certain things and Pug more for others. Whatever the case, you can use as many view engines as you like. The only catch is you will have to be explicit for what files you want to render.
Recall from the docs concerning app.set(name,value)
, the 'view engine'
name
takes one value
, the extension to be used for a file name when the file name extension is omitted. Hence, if we declare
app.set('view engine', 'ejs');
and later invoke res.render('index');
, then we have basically told Express to assume there's an .ejs
extension on the end of the index
file name given to res.render
. If we did not use app.set('view engine', 'file-extension')
, then Express would not know what to do with res.render('index')
. Instead, we would have to explicitly (i.e., manually) include the filename extension like so: res.render('index.ejs')
. The upshot of all this is basically we should use app.set('view engine', 'file-extension')
to tell Express the default template engine we want to use when filenames are provided to res.render
when the file extension for the file name is omitted. If you want to use a view engine other than the default one set by app.set('view engine', 'file-extension')
, then you must explicitly provide the file extension for whatever file name you pass into res.render
.
Here's the process in more detail with a working server example below it:
NPM install the engines you need:
npm install ejs
npm install pug
npm install hbsSet the engine you want to be your default view engine:
In the commented out JS code block below, uncomment whichever line you want to set your default view engine.
Whichever one you uncomment means you do not need to provide the file extension for that file when the file name is passed to
res.render
. For example, if you uncommented the line setting ejs to the default view engine, then you could useres.render('index')
and Express would automatically look forindex.ejs
If you do not uncomment one of the lines below, then you will always have to manually specify the file extension for the file name passed to
res.render
.If you set ejs as the default view engine, then whenever you want to use hbs, pug, or something else, then you will need to explicitly provide the file extension for the file name passed to
res.render
.In summary, if we set ejs to be the default view engine, then we could use ejs, hbs, and pug in our application like so:
res.render('index'); // assumes the file is index.ejs
res.render('index.hbs'); // explicitly tell Express to use Handblebars
res.render('index.pug'); // explicitly tell Express to use Pug
/* Uncomment one of the lines below to set your DEFAULT view engine */
// app.set('view engine', 'ejs'); // EJS
// app.set('view engine', 'hbs'); // Handlebars
// app.set('view engine', 'pug'); // Pug
Render your template (be sure to set the file extension when rendering a template without the default extension):
res.render('index'); // assumes default use of .ejs extension per above
res.render('index.hbs'); // specify use of handlebars as hbs is not the default
res.render('index.pug'); // specify use of pug as pug is not the default
Finally, here's an example putting all of this together (see below the code block for directory structure and file contents):
// server.js
/* native node modules */
const path = require('path');
/* third-party node modules */
const express = require('express');
const app = express();
const helmet = require('helmet');
/* application-level middleware */
app.use(helmet());
app.use(express.static('public'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
/* configure view engine settings */
// Set the default view engine
app.set('view engine', 'ejs'); // uncomment to make default
// app.set('view engine', 'hbs'); // uncomment to make default
// app.set('view engine', 'pug'); // uncomment to make default
// Specify folders for Express to look in for views files
app.set('views', [
path.join(__dirname, 'views'),
path.join(__dirname, 'views/ejsViews'),
path.join(__dirname, 'views/handlebarsViews'),
path.join(__dirname, 'views/pugViews'),
path.join(__dirname, 'viewsFakeOne'),
]);
// For visit to the host root http://localhost:3000 (load index.ejs since ejs is the default view engine)
app.get('/', (req, res, next) => {
res.render('index');
})
// For all routes below, { name: ... } is being used as the "locals" argument to pass data to the rendered view
// For files directly in the views folder (which will be typical)
app.get('/sampleejs', (req, res, next) => {
res.render('sample', { name: 'EJS' });
})
app.get('/samplehandlebars', (req, res, next) => {
res.render('sample.hbs', { name: 'HANDLEBARS' });
})
app.get('/samplepug', (req, res, next) => {
res.render('sample.pug', { name: 'PUG' });
})
// For files in subdirectories of the views folder (somewhat common)
app.get('/subfolderejs', (req, res, next) => {
res.render('subEjsView', { name: 'EJS in a subfolder' });
})
app.get('/subfolderhandlebars', (req, res, next) => {
res.render('subHandlebarsView.hbs', { name: 'HANDLEBARS in a subfolder' });
})
app.get('/subfolderpug', (req, res, next) => {
res.render('subPugView.pug', { name: 'PUG in a subfolder' });
})
// For a file in a viewsFakeOne folder not within views folder
app.get('/fakeview', (req, res, next) => {
res.render('fakeview', { name: 'EJS in a viewsFakeOne directory' });
})
app.listen(3000)
Directory structure needed for this code:
express201
├── node_modules
│ ├── ...
├── views
│ ├── ejsViews
│ │ └── subEjsView.ejs
│ ├── handlebarsViews
│ │ └── subHandlebarsView.hbs
│ ├── index.ejs
│ ├── pugViews
│ │ └── subPugView.pug
│ ├── sample.ejs
│ ├── sample.hbs
│ └── sample.pug
├── viewsFakeOne
│ └── fakeview.ejs
├── package-lock.json
├── package.json
└── server.js
File contents:
// subEjsView.ejs
<h1>A template file rendered using <%= name %>!</h1>
// subHandlebarsView.hbs
<h1>A template file rendered using {{name}}!</h1>
// subPugView.pug
h1 A template file rendered using #{name}!
// index.ejs
<h1>Rendered template page!</h1>
// sample.ejs
<h1>A template file rendered using <%= name %>!</h1>
// sample.hbs
<h1>A template file rendered using {{name}}!</h1>
// sample.pug
h1 A template file rendered using #{name}!
// fakeview.ejs
<h1>A template file rendered using <%= name %>!</h1>
// server.js (the code snippet previously)
Statically serving an entire front-end site
As mentioned previously, we can host entire frontend sites using Express by just dumping everything in a public
folder and statically serving it. Execute the following commands in the terminal (e.g., bash):
# Navigate to desktop or wherever you want the project to be created
cd ~/Desktop
# Clone the repository at https://github.com/rbunch-dc/jquery-todo
git clone https://github.com/rbunch-dc/jquery-todo.git
cd jquery-todo
# Get rid of the git repository--we don't need it for this example
rm -rf .git
mkdir public
touch server.js
# Initialize a project using npm
npm init -y
npm i express helmet
npm i nodemon -g # if you haven't already
nodemon server.js
Now, move all of the files in the directory except server.js
into the public
folder (using VSCode or whatever editor you are using) and paste the following code into the empty server.js
folder and save the file:
// server.js
const express = require('express');
const app = express();
const helmet = require('helmet');
app.use(helmet());
app.use(express.static('public'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.get('/', (req, res, next) => {
res.send('index.html')
})
app.listen(3000);
Now navigate to http://localhost:3000
and behold!
Rendering
A preface about rendering in general
When using send
, sendFile
, or express.static
, we are always sending back HTML, CSS, and JavaScript. Why? Because the browser is on the other end and it is expecting ... HTML, CSS, and JavaScript. The goal of Express is to always get to this point somehow (i.e., sending back HTML, CSS, and JavaScript). The goal of Express will be either to send back JSON (like in the case of using React or some modern rendering for single page applications) or likely to send back something that is not a static product. That is, we do not want to send back static code (as is the case when just dumping a bunch of code in the public
folder). What if instead of using JavaScript to manipulate the DOM, what if we actually wrote the DOM the way that we wanted it from the beginning? So when the response is initially sent out, instead of having something where blanks are waiting to be filled in with JavaScript, what if instead of that the DOM were written so that when it showed up it showed up in the correct way? Then we wouldn't need JavaScript to get involved because the DOM would already be correct. We wouldn't need that extra step. But in order to know what would be in the blanks, we need information from Node. In order to pull that off, if we're not going to use JavaScript to manipulate the DOM directly, then we need something in between that can speak both Node and front-end. That thing that speaks both Node and front-end ... that thing is a template engine.
When a request comes in, Express does its thing internally (our routes and stuff). Before the response goes out, we send a template some Node (i.e., some data from Express) for the template engine to make sense of. Since the template engine speaks both Node and front-end, it will build HTML, CSS, and JavaScript for us, and then it will take that final product and send it back out as the response. So the main job of the template engine is to marry the data from Express to the front-end so that what we get in the end is not a static front-end site but a dynamic front-end site where we can build the DOM based on Node.js.
In a previous note we talked about a variety of things that went on to use res.render
. Recall the last 3 parts:
- Give
res.render
a file and data: We pass thatres.render
two things: 1. The file we want to use (e.g., an.ejs
file, a.mustache
file, a.handlebars
or.hbs
file, a.pug
file, etc.). 2. The data we want to send to that file. - Combine Node code with HTML/CSS/JS via view engine: Express uses the node module for our specified view engine and parses the file accordingly. That means it takes the HTML, CSS, and JavaScript and combines it with whatever "node" there is in the file (i.e., the data available in
res.locals
). - Finished product (just HTML/CSS/JS): The final result of this process is a compiled product of the things the browser can read (i.e., HTML, CSS, and JavaScript).
Note how step 5 is the translation part. It is where Express uses the node module for the specified view engine to parse the template file from a little bit of Node and some HTML, CSS, and JavaScript to only HTML, CSS, and JavaScript.
A note about res.locals
and passing data in res.render
As noted in the docs for res.render(view [, locals] [, callback])
: Renders a view
and sends the rendered HTML string to the client. Optional parameters:
locals
, an object whose properties define local variables for the view.callback
, a callback function. If provided, the method returns both the possible error and rendered string, but does not perform an automated response. When an error occurs, the method invokesnext(err)
internally.
There's more from the docs, but what's important to us is the first point about locals
. Whatever data is passed to res.render
as the second argument is automatically appended to the locals
object available in whatever view we are dealing with. Not only that but we can use the data property names directly for their values instead of having to worry about a bunch of destructuring (unless we want to).
As an example, suppose we have the following in our server file:
app.get('/', (req, res) => {
res.render('index', {
msg: 'Here is a message.',
secret: 'This is a secret',
friends: ['John', 'Jeff', 'Eric']
});
});
And suppose our EJS view is like this:
<h1>Silly example of locals object and its use!</h1>
<h2>The message</h2>
<li><%= msg %></li>
<h2>The secret</h2>
<li><%= secret %></li>
<h2>The friends</h2>
<li>As a basic array: <%= JSON.stringify(friends) %>. But below we list them:</li>
<% for (let i = 0; i < friends.length; i++) { %>
<li><%= friends[i] %></li>
<% } %>
<h2>The entire locals object</h2>
<pre><%= JSON.stringify(locals, null, 2) %></pre>
Then what you will see will be something like the following:
Look at all of the properties on the locals
object! In particular, note how our data was appended to the locals
object and made available as local variables in the view. We didn't have to use locals.msg
or anything like that. We also never had to do anything like res.locals
because it is already a given that we are inside of the response
object (i.e., res
) when dealing with our view. This should make sense because we are, after all, inside of res.render
when specifying what data gets passed to the view template. In fact, trying to access res.locals
won't even work inside of the template file because that's basically the same as trying to do res.res.locals
since we are already inside of the response object. So when using locals
just remember you are already inside of the response
object and that your data is accessible directly using the data property names instead of locals.<property-name>
.
That is why templating is powerful. We essentially have a bridge between our .ejs
, .hbs
, .pug
or whatever template file/engine you use and the Express server. The locals
made available in the template comes from the route visited by the user (and whatever middleware is involved in that process). When we're ready to build the DOM, we need something from Express, and we'll write it to our response before it's ever sent out so the browser will never have any idea what's going on behind the scenes.
It's important to note that res.locals
lives throughout the lifetime of preparing the response
(i.e., res
). When we are actually ready to render and use res.render
, the second argument passed in to res.render
, if any, will simply be appended to whatever res.locals
already is up to that point; that is, we have lots of opportunities between receiving the request
and calling res.render
to modify the res.locals
object using middleware.
Let's consider a somewhat contrived example to make this more concrete. Suppose we have the following in our index.ejs
file:
<h2><%= bannerMsg %></h2>
<h2><%= _locals.bannerMsg %></h2>
<p style=<%= userType === 'premium' ? 'color:green;' : 'color:red;' %>> <%= greetingMsg %> </p>
<h2>The entire locals object</h2>
<pre><%= JSON.stringify(locals, null, 2) %></pre>
And we have the following in our Express server file:
const path = require('path');
const express = require('express');
const app = express();
const helmet = require('helmet');
app.use(helmet());
app.set('views', path.join(__dirname + '/practice-views'));
app.set('view engine', 'ejs');
app.use(express.static('public'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
function randomNumInclusive(min,max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function validateUser(req, res, next) {
res.locals.validated = (randomNumInclusive(0,1) === 1 ? true : false);
next();
}
function userType(req, res, next) {
let {validated} = res.locals;
res.locals.userType = (validated ? 'premium' : 'basic');
next();
}
function greetingMsg(req, res, next) {
let {userType} = res.locals;
res.locals.greetingMsg = (userType === 'premium' ?
'Thanks for using premium! Drop us a line if you see room for improvement.'
: 'Enjoying your basic plan? Consider upgrading to premium!')
res.locals.bannerMsg = 'bannerMsg set on res.locals during middleware sequence'
next();
}
app.get('/', validateUser);
app.get('/', userType);
app.get('/', greetingMsg);
app.get('/', (req, res, next) => {
res.render('index', {
bannerMsg: 'bannerMsg passed manually in second argument in res.render'
});
});
app.listen(3000);
First let's take a look at the two possible outcomes and then tease apart what all is happening/happened:
The randomNumInclusive
function is only meant to ensure we get a random number between 0 and 1, inclusive, to simulate randomly determining whether or not a user is validated so as to start the cascading effect that will become apparent:
app.get('/', validateUser);
: Express knows to run thevalidateUser
middleware as soon as a GET request is made to the root. We randomly determine whether or not the user is validated and store the result on theres.locals
object asvalidate: true/false
. Sincenext()
is in this middleware, control gets handed off to the next piece of middleware.app.get('/', userType);
: This is thenext()
piece of middleware referred to at the end ofvalidateUser
. TheuserType
,premium
orbasic
, is determined at this step based on whether or not the user was (randomly) validated from the previous piece of middleware. Note how we actively useres.locals
in this piece of middleware to further add data tores.locals
in the form of whatuserType
we have. If the user is validated, then theuserType
is set topremium
. If not, thenuserType
is set tobasic
. Control is now passed to the next piece of middleware.app.get('/', greetingMsg);
: This is thenext()
piece of middleware referred to at the end ofuserType
. A greeting message is formed based on whatuserType
we have. Control is now passed to the next and final piece of middleware whereres.render
is called.- We use
res.render
: At this point,res.locals
has a number of different properties and corresponding values on it. Note that we do not have to passres.render
a second argument in order to have access tolocals
as local variables in the template file. That is, when we callres.render
, it is up to us if we want to pass additional data to be appended tores.locals
. At this point, we can use anything/everything on thelocals
object in the template file.
Note: Properties and values manually set on res.locals
throughout the middleware process before calling res.render
are available as local variables in a template file, but what if one of the property names manually set on res.locals
conflicts with a property of the same name given as part of the second argument to res.render
? If we use the variable name just on its own, the last one assigned "wins" or takes precedence. But note that we can still access the one we set manually by accessing it through the _locals
object on res.locals
. Properties and values manually set on res.locals
are appended to the _locals
object on res.locals
. Hence, if we still want to access the property value we manually set, then we will need to use _locals.<property-name>
. For example, suppose we set
res.locals.bannerMsg = 'bannerMsg set on res.locals during middleware sequence';
somewhere in the middleware process and later invoked the res.render
method like so:
res.render('index', {bannerMsg: 'bannerMsg passed manually in second argument in res.render'});
If we just try to access bannerMsg
in our template file, then we will get 'bannerMsg passed manually in second argument in res.render'
. However, we can access _locals.bannerMsg
and that will give us 'bannerMsg set on res.locals during middleware sequence'
.
Passing data that we trust (example of unescaping HTML using EJS)
As the EJS docs note:
<%= val %>
: Outputs the unescaped value ofval
into the template (HTML escaped)<%- val %>
: Outputs the unescaped value ofval
into the template
How could this be useful? Well, suppose we have some HTML in our database that we want to retrieve and drop into our template. The default way of dropping stuff into the DOM <%= val %>
will result in literally printing the HTML string in the DOM (since the HTML is escaped for <%= val %>
) which is obviously not what we want. We don't want to escape the HTML in this case. We want the browser to interpret it. Hence, in such cases, we use <%- val %>
.
Why would we normally not want to use <%- val %>
? Well, HTML is generally considered unsafe because if Express or the templating engine doesn't know where the HTML came from, then it could be that someone sneaked a script
tag in there and is trying to do some kind of cross-origin attack or something of that nature. So by default HTML is escaped. But if you want the HTML to be evaluated and you trust the source of the HTML, like if you yourself are pulling it from your own database, then instead of using <%= val %>
you will use <%- val %>
. This will print off the HTML and tell the browser to interpret it rather than to just print the string off as text. Basically, using <%- val %>
indicates that you trust the data. You know that it's safe because it's coming from us somehow or we just trust it for whatever reason. It is the only way to get escaped data out and to have it print as desired.
Consider an example of having a company directory where all the pictures were stored in base64-encoded format. So in the database you'd have a large string representing a picture and anytime you'd want to drop the picture in the DOM, well you would want to use <%- val %>
very likely.
Example with base64-encoded picture
The .ejs
file:
<h1><%= msg %></h1>
<h2><%= msg2 %></h2>
<h3><%= validated %></h3>
<%- html %>
The Express server.js
file:
const path = require('path')
const express = require('express');
const app = express();
const helmet = require('helmet');
app.use(helmet());
app.use(express.static('public'))
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.set('view engine', 'ejs')
app.set('views',path.join(__dirname, 'practice-views'))
app.get('/about',(req, res, next)=>{
res.render('about',{})
})
function validateUser(req, res, next) {
res.locals.validated = true;
next();
}
app.get('/', validateUser);
app.get('/',(req, res, next)=>{
// the data, in the 2nd arg, is going to be appended to res.locals
res.render("indexWithUnescapedHTML", {
msg: "Failure!",
msg2: "Success!",
// HTML came from the DB and we want to drop it in the template
html: `<p><img src="data:image/jpeg;base64,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" /></p>`
})
})
app.listen(3000)
Include other files for more robust templating (example using EJS)
One thing that's awesome about template files is the ability to pass around code amongst the templating files themselves. That is, basically you can make template files to make your code more modular. Maybe you always want the head for your HTML files to be the same, the navbar the same, etc.
For example, suppose we make a head.ejs
file where we will put regular HTML header stuff (the idea is that we want this to appear on every page, hence the template):
<!-- head.ejs -->
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="/css/styles.css">
</head>
We can now create another template for the navbar:
<!-- navbar.ejs -->
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">WebSiteName</a>
</div>
<ul class="nav navbar-nav">
<li class="active"><a href="#">Home</a></li>
<li><a href="#">Page 1</a></li>
<li><a href="#">Page 2</a></li>
<li><a href="#">Page 3</a></li>
</ul>
</div>
</nav>
Finally, our index.ejs
would look something like the following (make sure to unescape the HTML you are including):
<!-- index.ejs -->
<%- include('./head') %>
<%- include('./navbar') %>
<h1>Home page!</h1>
<%= msg %>
As the last step we have our Express server.js
file we have been making every step of the way:
const path = require('path');
const express = require('express');
const app = express();
const helmet = require('helmet');
app.use(helmet());
app.set('views', path.join(__dirname + '/practice-views'));
app.set('view engine', 'ejs');
app.use(express.static('public'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.get('/', (req, res, next) => {
res.render('index', {
msg: 'a message'
});
});
app.listen(3000);
Using <%- include('./head') %>
is the equivalent of basically copying and pasting all of the code in the head.ejs
file and dumping it at the top of the index.ejs
file. The idea is that if you have multiple pages then you can just include
whatever you want on whatever page and keep things nice and tidy. Assuming we have app.set('view engine', 'ejs');
in our Express server, which we do, then we can omit the file extension when we use include
with EJS. If, however, EJS is not the default engine, then you will need to include the file extension.
Finally, we could easily make an about page by including the head and navbar and then something else by making an about.ejs
file like so:
<%- include('./head.ejs') %>
<%- include('./navbar.ejs') %>
<h1>About page!</h1>
<%= msg %>
Then we can add the following to our Express server:
app.get('/about', (req, res, next) => {
res.render('about', {
msg: 'an about message'
})
})
The effect is that our about and home pages look the same in the way that we want them to look the same.
Adding styles, scripts, etc., to template files (properly setting href
, src
, etc.)
Suppose your directory structure looks something like the following:
project
┣ node_modules
┣ public
┃ ┗ stylesheets
┃ ┃ ┗ styles.css
┣ views
┃ ┣ login.ejs
┃ ┗ welcome.ejs
┣ package-lock.json
┗ package.json
Within login.ejs
we may want to use some styles from our stylesheets
directory. Why might it make sense to have a link
tag as such:
<link rel="stylesheet" type="text/css" href="/stylesheets/styles.css">
The answer is that since the styles are statically being served from the public
directory and we have used app.use(express.static('public'))
, Express will look through the public
directory for a folder stylesheets
and a file styles.css
within that folder. Express knows to do this because when it looks through the public
folder the path is simply
/Users/danielfarlow/Desktop/[...]/project/public
Hence, if we set the href
to href="/stylesheets/styles.css"
, this is equivalent to
/Users/danielfarlow/Desktop/[...]/project/public/stylesheets/styles.css
The same principle can be applied to scripts or whatever else you may have in your public
folder or wherever it is you are statically serving files from.
Basic examples (using EJS, Handlebars, and Pug)
The best way to learn is to consult the docs for each view engine:
Since most of the previous notes relate to using EJS, the examples with Handlebars and Pug are more sparse. Nonetheless, hbsPractice.js
and pugPractice.js
files have been provided as the Express server, where they are intended to serve up the index
file in the practice-views
folder accordingly (i.e., index.hbs
for handlebars and index.pug
for pug). The strictly necessary code blocks/files are provided below for each view engine though. In each case, the index
file is the view being rendered while the other file acts as the server.js
file that we need to start up with node
or nodemon
.
EJS Rendering Example
index.ejs
<h1><%= msg %></h1>
<h2><%= msg2 %></h2>
<h3><%= validated %></h3>
<%- html %>
ejsPractice.js
const path = require('path')
const express = require('express');
const app = express();
const helmet = require('helmet');
app.use(helmet());
app.use(express.static('public'))
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.set('view engine', 'ejs')
app.set('views', path.join(__dirname, 'practice-views'))
app.get('/about', (req, res, next) => {
res.render('about', {})
})
function validateUser(req, res, next) {
res.locals.validated = true;
next();
}
app.get('/', validateUser);
app.get('/', (req, res, next) => {
// the data, in the 2nd arg, is going to be appened to res.locals
res.render("indexWithUnescapedHTML", {
msg: "Failure!",
msg2: "Success!",
// HTML came from the DB and we want to drop it in the template
html: `<p><img src="data:image/jpeg;base64,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" /></p>`
})
})
app.listen(3000)
Handlebars Rendering Example
index.hbs
<h1>HBS Rendered file!</h1>
Your message is: {{msg}} !!!
{{{html}}}
{{!-- Country name: {{country.name}}
Country capital: {{country.capital}} --}}
{{!-- each, if, unless --}}
{{#each countries}}
{{#unless this.western}}
<li>{{this.name}} -- {{this.capital}}</li>
{{/unless}}
{{/each}}
hbsPractice.js
const path = require('path')
const express = require('express');
const app = express();
const helmet = require('helmet');
app.use(helmet());
app.use(express.static('public'))
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.set('view engine', 'hbs')
app.set('views', path.join(__dirname, 'practice-views'))
app.get('/about', (req, res, next) => {
res.render('about', {})
})
function validateUser(req, res, next) {
res.locals.validated = true;
next();
}
app.get('/', validateUser);
app.get('/', (req, res, next) => {
// the data, in the 2nd arg, is going to be appened to res.locals
res.render("index", {
countries: [
{
name: 'Russia',
capital: 'Moscow',
western: false
},
{
name: 'England',
capital: 'London',
western: true
}
],
msg: "Failure!",
msg2: "Success!",
// HTML came from the DB and we want to drop it in the template
html: `<p><img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxMTEhUTEhMVFhUWGRoWFxgXGBgXFxgaGRkdGBoYGhcYHSggGxolHxgeIjEhJSkrLi4uFyAzODMtNygtLisBCgoKDg0OGxAQGy0lICYtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIAKMBNgMBIgACEQEDEQH/xAAcAAACAgMBAQAAAAAAAAAAAAAEBQMGAAIHAQj/xAA+EAABAgUCBAQEAwcDBQADAAABAhEAAxIhMQRBBSJRYRNxgZEGMqGxQsHwByNSYoLR4RRy8RUzQ5KiFiRz/8QAGgEAAwEBAQEAAAAAAAAAAAAAAQIDAAQFBv/EADARAAICAQMCBAUEAgMBAAAAAAABAhEDEiExBEETIjJRBRRhcYFCkaHB4fBisdEj/9oADAMBAAIRAxEAPwBspIGdsxlEGTuFBKiGRcklgQOQBWXyAw8rRojRhPIEBhdSU3JAwkdz54j1X8X/AOP8njL4I3zP+P8AJBK0ylB0gkdRjLZxG6tCsAkpYAEm6cDO8EanWTDNQkEUvSzuAwKnLNskh+4y7R5xfV0ghLAMz3Pfa4iMvi2V+lJF4fBMP6m3+yF5KRYliWDHN8Wj0I6QtnzWKXKSupIPKMEhnLZAL38oNQlkrDutKD4fVTKLlZdiA9nYCrNorj+LS/XH9iOX4JGv/nL9/wDBL4cZ4ce6NalJBIB6kY3v5WidAcAjBxHp4uqx5V5WePn6PLgfnX57A1EZ4cE0RlEX1nPpBqIyiCaIyiNqBpBgiPaIIojKY2oOkHojKIJojKIGo2kHojKIIpjKI2o2kHoibSaVS1BKQ5+3fyjaiLD8OaNkqWXDlh5Df3+0Rz5/Dg2dPS9P4uRR7dwTiXCxKlg1FS1ED+W8DS+BKJDqDHPaLaUA2IeNJ/Dpa9qT1TY+o3jzYdbJKm/yezk+Hwk7S29ivztNKkC6aiUtlwo72MKp8kG6bC1ovo0KCgJUHaz49ukezOHSlAAoBCcdoMOuUebbFyfDnNUqS7Kv7OeoqTcOAfrFk4F4tLlm7m/tD9XD5TjkTYMxAYeQwImRp0gMAITP10cka0j9N8PlileoglahsxMrUDrGqdNtA+qkDeOCotnpbpEpnjrHqZzwBL0yTa4gyVowncmM0kZNs3SY3AiQoAEaiFsNGJX1iUKgeamNDNAOY1BDgqPCuIc7tEUySBcOT5wtBC6o0MAf61vmYdojm8Vlp/EPeDpYLQxCYyEp+JJIsVRkHRL2BqXuKdYlqi5c1FiQRZ3BD/oGB+HoUSlmfJOX6ntfz+hjaTrUz3Wlil1F0gitmBPNkd8Fu0eTAU00kDfrupnDYdz1tHK2dUUTaWQklSS5NYU7H+l2s7jNsDdoG4/oSmWp/lSa3CsAXdlY3t3HeC+H6pc1KyaOYoDgEDcv5sAfWCZ2lMyyjZjU+LkPtccpy/q0ZOmY5ZrZ6gqaJtQCEeJUQXdJsw7t9odfCWpUrWrXY+HLuQXCSZiVbAufmtkti8DfGOmQk6ikC6cAsFIT84ANiaUE9W8hHvwnLCkzAlx4xDKs4R4RLkAkO8wsC7knpHTtosl+ouqp6WWpCHUfEKE0l7l1czMkgJxfs7xTuHadUvTeOqaqozly0osoMlVO2TY33i68I1EtRUkEApXyXYmpDKAG4ZzFN+KJdMuTJQ1KtROYIJJNSFG4AZgZiPXyiOOTTpDTimtw/h3EBMRWHKQsyyprFQewxbv2Ig6WUqBIIt3H5GFkvThEiVISWVKleMQRyhZW4KwMhIJLA7+TL9TpCJ2nlpWQJiFplklSnmJQJiSoOAXLDfLdX9DF12SLq7X1PNz/AAzDPdKn9P8Awsxlx54cLfggzFaX94VKKJq5QUrJoax8nb0h8ZcevDLqVnz+TBok4vsB+HGeHBfhxnhw2sTQC+HGeHBfhxnhxtZtBDp9NUcs0ZOkMcNBCEXgvUTkkCztE3kakXhig4O9mD6HTprlgpck36N/eLaUggCEmhmoUoJpAYZbfe8NioNYx53VScpKz2eihGMHVGx042MeCQXzEBnnrEnjvHNTOy0bgEWjZEzaNpbKiZSBCNjJGoMepAEakjrHhWIATcriGeQY0mL6QFqZ9NlQVEDZIWEFSZtormo1TbxAOM0/ieKvG2JrSLb48eTprByfeKbP+KqQSA7bO0U/W8Ymz1EzpzhJVyhglmFgh8uxFTnl2vEMnkKQ8x1wzXuMdYHXTu/oY4lxL4jMs0SSrIc3YkMQWBGDEnD/AI61SWTNZQ2Jdxfc9L9/KGg21bRpKuDsGrmy0IKitYA9foIR/wD5Cg/JMVuOYFOC2+9oruh+IU6j/wAibMWq/E+OtmuRC3i8mYo5AclylNsKDpc3ukZO4xE55dMqW6GULjbLFreIg/jc+cJp+t9Yj0+kDJQFpUpgXYJDszirftn1tBatRKkEeOjxUvfw/mSG9AS/lHRi6vG9nsQngkt1uLF6kxkWbhug0erFWmmhX8SFGmYjspOez46ExkdilBrk5nadUx5M4dQUc5SlgFBICU8uXG34vp0gbUSQbYLu5ybAbbWeLDOkBVTi298uHVnz+8KdRpkJU7GwTdiQ5wXFzjytHhSPYgwbh0xSSlDBnUqq7uRkWt/n3aaxIqUoMWSE3wCCd/JR9oEl6b94hRAuDc9lJxb2Hcwi+Nfin/TDw5bFbgltrP8AkIMIuTpGlJLdlV/aNMCZiQSaihYF9wAlz/8AXvBHwryolAH+FYD3ShICiS24TMXbofSKxxviX+pWkrZ2KX6VKz5t9YtfACkTgjZWnow7HwpUsgY8o6mnGCTI3cm0WDgqvBnVBNSl+JbNvEIACujpWf6ugYgyp0tQCxLKiJ3hrWoAqNCqyEPegFQTsOXtD+ZogCgpDEkOCXIYzFEepmHHSKTKmpPiE1ciyE9CSlK7XZ6lmIcj8Fq03DqUlKAkqpKVG4qC6UgktlkgenlFY+IJ/hK08xYA8DUeI6X+TxUco/pf0SItnAjypIIKVJKVF3D3UD7ERWP2gyuUqexQFY/iCVel42F+en7hyenYZ/s1WoydXKZ6dQVA/wC4AEf/ACD6xbUcOUSA2b+kU/8AZbq2RN/mXUbZPhSX+pV7x0iTNqAKfWPUjllGOx5OXpoZJ2yCRwhG7mN5nCJZwCPI/wB4NqiR3ESead3ZZdNiqtKFsvgiGuSfpHiuCo2JhmFRgMDxsnuD5XDXpQnVwI7KHqIjHBVDLN2h+lUbQfmciF+Sw+wq02jQAzX67+8TI0jAh/KJNZpiQSlVPpC9OqFIFbKILZIcRrlLdMpphDajzUS2e8AjWsWjTU6hSSQth+Y6wErVAiLxx7bk3P2HUvW2cFo2VxE7mK1OmrSbJMQL4gpOQRG8BM3jFnPEB1iKZxLoYqs3ipNmiRGi1C8S1X62jeAlybxr4G2p4uRuBCvVcXfcn1gKbw3UHEpR8gT9oEPD5xfkVbLgxRY4iubJpmpKjYmB5iepjVeinJ2aIRpZijf6xRRQrkwzSyUXUpykDYAuXDAA7wu49wNAWGZD1FJfLnlDt8twItGk0yaES0tUlJKnwVE2fsGvE2s0SwQ6HYEWwEiwZti/6aPEzZnObZ6mPGoxo5BxLReGq5vv/gx6RUAbve0OPibTpVMoSXULkuOgt3hMUGW6VWIv+hF4O1uTmqewLOTQsKHuDce0Fj4imAEHmSb3d3YDO1hAmsll3PoAIGUkHI/tDShGXJPcYTPiWccMLuw+zn8+kRDiCpvKtbjzL/5gI6azgvEaUGF8OK4QLZZtAlKQ4W2zk/3/AFmMgf4f4L4oUtRNI5drntdyA0ZHPKCvdlYp0fQa5YUgAXSUi4IIIZreYAbzgXWyTUL9G6ctX9zf0vHM/g/imo08j9yjxELIdCybXL0kfI9QBLEOnGSekcK4nL1aEKRYlxMSSCqWd0qT1sWcXF4jJMvGQt+J+ITdNKPhh7fMTZyWNh269I51xXhswpVPnElaklXMC7Uslh1YfaOx8S4agpIIJDXvsL72JhT8TSkHSzCaflDdGy3k/wB4tjloXAslqOCzSUqcOG94s/whxWvVaRJZ3MvsxrU/YlQT7CE2v4V4SJRJNUxJUQcghs+ZUfaF+kWZcyWpNik58o7pRU4nMpOLPoGYmoSTg1qGeqFW87GKPr9GAqYgnlVNqWpmZLqlpSGs5Mn/AOxvDn4U+I0TZKgtQTNlzamP4k11W9CR5QP8UpCJimAdc0m7u0v95LADsAK1PbYR58U4y0s6m01Zv8N6o+DOQpgBPUkMC4FKbX2YpHqYX/G8oK0wVY0oCerKASCfYEx7wLTkSJ/OFKK/EUQXasJ6YahmvG3EJR1GnWkD/tOJjFsoLC+XKVY2F2g1U7+puY0J+E8WTopennEFUta5aZiQ7qQvTpqUP5kqlhurEbvHYuD62ROlCZIWmZLVgpxaxBGQRuDcRwj4ikFGikJUlplcslzsJBSBYswUFe5iX9nPxWNBNWmc/gTSApgTQsfLMYbM4IFyKc0gHuUW4nG3TO/kiNawIXydQFpSuWakrDpUkulQOCCMiNZqVdDAUDOQzCnxGPAUjUsGMaK4gxuA0DQzakHKlqyC0b6dRcgm8L1cUBxGydaI2iVbh1K9hqRAsxIJZQFsRpL1Y6xtM1aDkiESaYzaZHquHpXlr5sPvA2i4HLQai58y4jbWalkjw1PAaOKlINYsLv084vFZHHZkpOCluhrN4cgsQkWjxWhCvmCe3KD94qOq/aBpkVcy1EBwEBwe1Rt9YXK49xTUSwuSZMiWqpnYrZId3UDYg2YfhPqHimuXX3CskHwi/6fh6Umo0MP5QG9YW8a+MdFpVFEyaCsZQgVqFwLtYZdiXYGOZzNDxGepVeoWoEFKyFmkggOmkMMKuOx3EAzuBhEyguWy3ufVoPhxvzSv7A1yS2VFw4j+1RDNptOpy/NNYAH/agl/cRW5vxPrlEqGoUCdqUAObsE0sfvGiNHJQAxBKnZwbixvfIBfa46QXLnJqCkILfKLkVWbF+j52HWIPqYQlUY39SiwzkrbDD8W6mXLbUSErUQChSVABT4dIf6e0aaTjU+csVSBKkOAWS6ySeUDo5IexYPvGqDVdfzJsks/Km1z1cYG2N4O0iHP8qQLepwzM2b9RAl1SapKhl07TtjXRz0pcgqGLnOS4uB+Fh+nio8Y+K5pUpEokJAp6vdi5/PvF2EpKZUxRyA/wDj6Rz3h/Cpk6YaQwrqc9HufS/tEMairbLTt8CCbLWtXMOb1uWYnptt+cQf6dS1iq7Wu4LDZ3i2DQoQFK8RJUjmLF+Xm5h1xjoHxFb1E5YWSk2KiQCAC3Rh5xdT1cE9OkGPDpmwLD5T1HR3iCfplA3Hr5w74NqFFbHG72HT2EN+IcNUVUmWQbMC2/Rji4N9mhXNp0x4wVWijiSx+hg/hPCVT5gSCycqV0HX8osavhJDgqWcXADMds+u0WLh2kRJliWlBQrNTukhgSQcl7C/frCy6hJVECxNvcj0WgEtICBYBgHsBnZi8exbpelQhIbmxcEHbrZ48jieVWX0nIeF8RmadKpEyWVAhbTZbnmUoKdVuXDbQy+F9fOkLOolTEhBtMMwEgAP8+FEByas2s73GRJTNKaVpVLWAEqD2P8ACW/EXAdRIsbQMdYZJWkqBQsUlFyO72YAMLj16xbU3yc6dcnStT+0LRhSUaoLkqZRWkipIUklBS6bkEgsWDtdoz9oYI0aTINVdbUlwQJM2YCCDgUAiObccR/q0JdRBCqqqHyWJNw4JUHN7tDz4Y4rMQmRodcAJaSPBmgiikilMpVIBS5NitncgxSMotJrn2CnvRX/AIx1SipAalquXpZP0tAmr0YBKhhRJDdAsgf+zfWLb8T8J8Y1nAlCnupy7N1CT/mBdLwQqFLEcqGI2YUmw8n7kk7x1RmkkI4ttldnhlOCQ5JB9TDbi3xIudLQCf3iCTVvdID38n9O8R8e4GuWyrU3vuP0DCEJc+0PojNpianFUdCkatUopQmnwzppBSDcqWZQLXsAA5fckAQR8Ia5M08pDrFKknpUtAJH9aT6RzpWsmAJSVE0WTf8IHKB2H5RHoNVMQsLlqKSC9s5eJy6a0Os5e/2hcM/c+IAeRSH2dLFJt2UtP8A7HMc4nyrH9dYsk3juomS1S5igoLQU9wM+hsLxX1odJO946MMHFUyOSSb2Ld+y/4qXpdRL061gaeaohQVhCiGStJ/C5YHZvJ47pMQDkAx8qagDltYx2L4c+J5szTSjdSkpCFElyVI5aj3LP6wzwOcvKTeVQW50RchByIgmcLQoWJB84rmm4hqlKYC+W3bq0FK1upd6qUuzM56H29IDwzi/Uv3MssZL0sLXwNexEBTeGzu3uIF1vHZyTZSlAWYhN+pJT/xHmh4suatKAUgqUAUuSQP4sMRvmLKGVLU6JOeNulYbodLMqZSSAASVG48h3hlK0SFocLPsLen+YUa/VKpnSD/AAkF+gu79G+0I0cbXLlolyVEKAvgtVdr52vE5qTWq6Hi4rZqyw6jUokWmmp/lpd1dxew7m0J0zBOUCouCQQhzyi4ZQPzHqfbur0yJi1KKjUo8xUouT6m/tD34ZluVkj5WG1rKMRnNjwVvgB4pw2VM1EoTZdQWhQcOCKWITYi7FRhjMQmaihQFSVpHvazHBBIgviGmZcpf8Ki9suhSG9yD6RDNkkKcXIYt/ELOP7G7GE1XRfTVivW6dSZqzKpR4iTY4CgMjGQq9v/ABjpEEvSqXLWZigVB3LAD+LlLuxwO42h3PmIWpEwAhLBgcgMxsOl/b1hJxfWApZDAbkuRyuQHAwCM9e0JPJpQVC2INRJGCQ5U48nICSbWLE/3g/TIABckOScktgNcuHG3r2gnTcOC0pK03JJScMQbPfA3PYekMzSKlzFUDkALOenyp62L37v5+fK3wdSVEiQkHObHIuHvvb/ADE5ms4FAOM5DW/x5iIZIa7F7EAC42Fg9n+xhTM4rzULSLOl05ba2w8mibk3sM5KJYdNql00uaXZ7B7FwzAMSz2yTtEsvjGmlC5oKQwFJJuGazg9fXEV7x+UfvSWuSSV5ax6Xxb0jYaiWtDLAwS6kkKBYgtyh7sbjaNqF1LsK9VrkKUuqW6DYB83cEv9vtAqZskKcFTuDsQCLO7E+1/WC5HBFzTVJah7lVvO3obOX92uA4HJlyglCEKWGcgcyiQz5Jy1seUNqZNQlIr+lkKU60S00luYVXUC5IBJyHZt+m1g0qytJK7lIpThgB1beJOFvUZa0umk0+ZLEAO7XG20emSUpUz3JT3eo/kl42pydMslSs0WSpQcDBwMBj/aMkSHWoswa/nf+0MdPJYKe4YhOcm3qLxillyfTHbtBWzM2Qr1JS1JGBY37ux84yMKKji7dPeMjUKcl4TJBVMaZSBhNRCnB+blDpG7g3eDuOT60TJSQkqmhFADtUiYtSyCouHEynOEgHtshV2RQlNN7Wcqdi1LWZ+/lE+n1aS6CTYU3qA3Lur5mbMHxZKVo5u1CPh65mnKUTxSksErHMk8yWSSCwvub8rRYFJQUKTNKFOGUmxcEDuaT0s7h92geZPGEkhIId2a5BJYs7g9sg7vHmomS5aS6FFRUFFaUqIZwKXxso2xfpActTTWzMth5wGfOARInzQtKK6FfjIKbJWrekAsreq+A9n8AUkBIJCihNibXIFs3UYpJWhdPh1ZweYEn5Q/KCA9jj3s5+GPiOSH0WpWmXMSpNFbALBNk1Yr5hboxDsYvCTn+CiaDuI6ZPhlCkEghLKAuWlmpR658rRy/VIAqbZm9zHblfhwaVAgk2Zr+hxEXEPg7SzXNHzAFx/ufHXMdEMqg9xZQ1HE7KHcfbf7R6gRcfiD4O8B1I5kA+oeKnqtOUt+r3jsjJTVo55RcXR4gsX+v68oBmliUvbaGcsP9XB9IB18vfs3t/zDrYRgk5YP5R0P9kHFJImTtPOLA/vZdskClaSfIJIHZUcyTtBfD9YuTMTNTlJcdDsR5EOD2JhqtULZ9B67jsv/AMKWVitQGOgu+0IdUtRJK1kjJ/4gnhk2VrJEudLvWkgpb/slPz1kYa7blwRmNNNoDOnBH4RkAZZnPTf2BaK4XjgrRDKsknTIuEcC8X96tXhyk3VspTbDoP5u9oZyuPISpNElKUJwBkk8tz1ucu5ObvEXEv3dVJPgOUsWqChkhlPm7YhOCakhFT52cpIcqYe9+h3iOfIpq5P7L2+/1Hxw0bR/L9wnWasla1BBZZfnyOoIGevoIW1FFykNmwBAh0dAQEigFQy5ztu4fEb6S2E4DEBg/X9GORyb3Ojw7FnDOIoqIJpsWLX6DyPeHPBVqBb8JW5LNkAN0PWNDppS+VKAl9m6N08sGGQRSlASRYuXzZI9sZ7NAkxoxa5GM0AsD1ffYv8A59IhnyS1SBckWvcP9MdIi0QIUXUS4Ktmqexfc3AH+0RPPkvRUTlyRbH6+kT4LCriWmCArBIFtnKnZI7Yt5dIr/8A09uV7KdxaybPvaz+6jFh1TTVKqLhNz/uNzi4YcvoYEmzghZANJc1PihL1G5dyKQ+1XkIhm5Q8Ko2lg3WwUEg2TYsCx7PbHaBNTKCjynmIbo569xkt2gycVpCbBKDfvcszDu143lISljdTFwMWyWL/TsO8RcuxZLuK5elmVAKRWAak4SpJAGSpWHbyhR8U8OW5mUKYcqiRaxNw2LMf6vMRb9QQsp+ZJ5KiAOtTX2sxthUFaWaFINQaouAwcVAC/eIN72NKFqjlOk1CpbkM24IsYbaTTTtQUugBCCCrY5Zh1H9rxLxIy5+o1ISilOl065luWqZUEpKqM0hVk9mOIt/Ak+Lo5C1FlKRUGSN7i1sQ04uKUiWOCumey5cxDMlIpYCl2DWs3kP0I3nTCoF7EgAkeeG8w8NJswjDW++/wBe8aaGSiZLJDcxyD0chj+swqdlmB8NkAJBDAksDa3Yff0iAykqCCCxPNi5uXPaG8iWEpUN7gHu2frAOjk1eEP5b/8As59bn2h477isn1Mihs3Bt6pJ8sQGpnFIJJdxs+/pfeG+pkKVu4Zr5zs36tA0nhZASnCzUXy1rX8yC27GGT2sXuSSOGSxzAm75tv09I8hdx/4kkaIIROmJrI+UAkgDdrbiPYyjN9jWvc4dw4KJZxZySpTDYMA3qNswTrtdMljkVhQ5Q9JFyAQDdzscktvA0kXpSlyGJx73+8OtNU7WJ9n69H+0POSTto417BE7SoI/e85UEumyrubVAhwkk9MHyiHXamWmakXCqBSAaQWAcFsku93dj3dpptZSlUtYdCi7FCSpB/jQThQAe7u0KtdKlTG5gsOCDcE4Yl7gtkPe0STTf0/6HPJE2oIpWEJbCSHXSDmk5qVsc5G0K/iLgilqMxJQokDlBYsEtawBNvVosElIT4QKAlDsqkVWZyQqxFwOY5JAgLUy0A1iUFzLH94pIDWAVzWDAVZcF+rh8eRxlcTNbC/4X+L9RpFALUqbKAbwyoukBmCSbpAbAt26dj+FPimRrZf7pQEwBX7tRAWzguBuL5Ecw1fw3JWQUrUicS5KSMi6mSC1ux6XMK9JxESZvhzk86FOmZJFBqF6lIDObu4pUMuq0dOuGZeXn2BGTjyd11ukUpJSC1i7erern6RyT4m4NMlLUFhxkEYN4uHB/jVRpE8FSF/JNQkMdw6bYY2HMLOMw71kuXqQUIYslLLvcKB9mpx1eDhy6JUx5pTRxzRpu2Yg4ihINywP3LRZOO8BmaZaVlilXTDX/MfWK3x5FUqr+Ej7sPoqPTjJSWxySTQjQlncW3gmpCkgC5x0gSWQbHd79enrG8hCfXvDiFo+CviNOmrTMSpUpakKLM6SDzEpJuCG/8AQZjo8jjomrC9Or93QaSG5sOSSzXDdQx6mOQqSCAff9dXH2h/8FcREub4KiAiZYF25msHwKmA82jOopurM05bXRd0SlAOQCFEkkkAGxO/u4fDRFLnEXBQlJbZ1IDgvh37i5byMMOJ6BCTdyzs5LA5pYCyfaBNdozlCQzPl1Zb3vHDLI5O2W0aVSGmj4pKmqShyDfIYObm48jEmvQErLsP1b3ipodxt5Zft/eJ/wDUKS7qKi17kgHZi7E/zbfWNQFl9xno1rVODkMkkhOQ4ch+p7eWC0WLTS2LbX83Ny/3tFL02rMtYUGU2AruSbe598QTxDjS5hZIKEcpVgnYm+AY1WaM0luWKZxWVLWQtQSWJIAfsMbv9jG6uLSlEIQSVEhhfdyCSdgL532ikplElg7kjG5Nm7lzD7R6ZEqYE1VTmwPkTUMOzqV1NmeBJIMckmxkqVRiz3LW8k39fpC7j04IQVFNdQFbmzKJFup7d4acW1stAVMVgYA3Idh7ufSKRrCpQC5y/mIdrsPlCgmwwBYHBHURw5W7s6+I7BfDdXRKS8xVz8pU705ShOEg9BkOHi0Mp0seRiQNyTcMQbP/AGisS+CygkTELqTYrLpYWBATd6nvftuIfcPm0TvBJJAAEsqyUgXF/mI65DpfMSd8jwdKmNUKPMKQwUxu42YjGQQW2hVr9UZOkUpJ5mAT3UA2/wDsJh7KkuAMgEPkCxa3sIpXxtrqZaZIyhRX0ILzE+Vy3rCxWqSRRvYA/Z/pSZWuEwfPLloNzcTSST659oK+HeOytLJlada1GYXNKQ9AJWpIJWW6DIs2LOy+A0jwNWbEDwkpKRylgpiAOoIVfq8UTWSANZNwk+Ic784YDJGPpHXNak0yF6d0dM1HGULpTKYgq5izpLpHKM3JUPIAl+pWo4siQp5pCUEBRXezZDAE9SC+WGSBCbhEgSkpBcBCAhgfxMCpRaz4tsxES65Ysnma5LAuRh7DLfaOLSXTbRZlK+Zi989LPt5gQq0i6HqUAEu5J6kmKFreKTpc1SAoowSoKNSxLekkuxs1zfY4gDiHHJ0xJTMUS5Jf5diLgC4FTtaGjGicsq4Oj8b+KUaeSmYAeZAWhJsWqSAC9xnHYwPxf440oql+MtBWkhM1I+QuRkXBs4t0xHK9RqZiwhKlFdI5QeilVWJ2eMk6QlQBAblVYfrDRTSktyTyvsLePzZuonTJq1k85SkqJekE0uT2/OMhlM0oUTUW6xkWWalRFk2kU8xSSgAJI5gHBDEoc7G2/TvdxoRzikgABIbuoFy+W3iraLi6QCVVsWFLls5DWuCzsNrw+0+tEwqSlhSUszg5A6thQ6YjlyQd7oYzhmmmKSCT8qiS7gWdmcOrbzD5GRNdwEvWJxSoKKqQk0bFgHByMOc4ETzBNVNTXzS0uvLDlcBzYZpt0cxtpdZMmKJA5QqzqZ+V2djZyxs9zATnF6kzKjfWcKIIomkMrIFT5sWLG2xyzRvrNSlCkhCUrrJqILY81soXuH/KDCZjkpWm5ZLksDdx0288wFqNFLmMlRSQLgqDly5ISotdx2sPSGjJTaX8DV7Eum1CUsVSwFisBWQkFwGuwS1rj6wGsJmMUtOSlSXKQpkVAoqWCxQBdjZJpsTE+v0MxIC1ygoJDUJcg4AdnqDFwDm2IIRMUmwKi5NQqpHMdg7s9vJ4z8m7RjcJ8NDSpyiliVhQCweb8RA6kYIPcBxE/DVzEc+nmORZaU8ymObFLrGQOmbmB9DwhAqCCpCskKJKCLOzDkuBzOfSNfBEtS1LPhKyAAllIDNUXIId2OQO8GM13d/XuYM1GpTqP+6ZiEhIszhJckul3ANgGfHlCPiOlSqWsJL1DzIIyPoO8OUT0Ty5QFLDsaSpJDOHBZKvmTYl38hAWu4MqYCSkEpUBXLYLALgBRA5bnBBAa7O8d2HqHD1boScLWxzsDrGAEQ81/w7ORzJBmpZyUhyPMAl85STviFuo06kBJUGrBI62JSXGxt9RHfDNCe8Wczi1yb6XVKSDYKTuDkd43GoSThx0NnAuxgNKmvjyjCXiuoU7dN4qkCXMJSpC0qnopDqoASFhgmxFeM7Ydka/i2Qs/u5oQL5srOx22YxzVGvmpASmasJFwmo0jqwdo202vmo+VTb4H9o5FhpclpZLOmaTWyWN0qtsQ+x+1/WPZlBNlFmAAJHT/m3SOdJ43qLNNNuwv52v5xkvjWoGJqvVj9x2geFPtRO0dGWgf8ALO36+4jJYAs+On+I50OM6i/71Ubf9Z1GPEVgjuzuYDxT90bY6KhRCgUmlQwdx3eJ9JqVy3KSm7XIc2e3l/aKPwziGoJBXMISNyAx3Av5CGJ4/QjnZ2sB1vf2P1iMnOO3I6iuS1cS16VFSpykikJtsVEO3oANz/dJoOJomLKGASzH/cLuOlxjvFZVrpmpUVTDgOGxhg//AMiCdNolClyUqVghjm+/6tAeKKXme50LIXaRInSlzaGUUMlhUxqIPiUghiwZg7l7ncv/AEyjKmPTWpHjdFS1uR8wuLJSAf5CbiK/qFqVSKeZIpKwVAqYO5awvjziPQcUmylghZIJJXfJFndQPVif0OBofWk/oWWYNTIlqmKnkGpAPiMvlCiAxbfLd8vFN4lxheomTCUjnsAHwouwa57fm0XPUcTCypIUkIpCUi123USPoOsCpn6bhygpSCtakFSSmlSXdmpBBSnlN4thj3Y7dcD34I0SdJopkzUqoqLqKzT8opAuTu7Nl7C8cx1P7zU+PVZUyvBwVWLG+Id8d46vWEGZZIDJTgd2G/mYRayYEpNLkhkgNfDxpZHqqJKUvYvqtUmWPDSCspIR3qJYkpN33G1xcA2H4xxhEhI8QCq7XLMQCBV8pJJdgT54jmer4wWUUq5nHMHewzVkQHrNbP1BAmLUQSuZfuSVEeox2jLBJ87B8fbYP1HHApVL27kttvmI5/E0AFjUsJDW5XcP9PvASOFGuilXndsPnfB9oc8O+GAUlUwgFQICc0u4cv6GHlHFDdsluzNFOQtlMbUsTcMHcEev0gzSzCs8oNLtXsGew9Az9YIl8LTLHJUwDHepvPr7OYnSyUpQkANZhguXz2jllOPYNGitJKsZhOGsQL/fEZEpPVBLWalz0BNux94yJa2MUGTqEBwQQKaSzVEW3I7Q44LqEJWlO6iFMS7gfkyYrstYTm584MSkppKQXKHD7BzYe0ejOOrYQdy5lU9kqKUkqrTUSFAJAAbAwfQw3la1K0qloepaSpZzZRpAA2Nn9YrPD9cEgmYVJuySUqIJAFQyL3HvDTQ6yWCpctAS2L3OWy4AGRjG0c2SH8BH+jUshbig1uSTjmSLMdw/0gLhgmgkzJktRIC/lJLlL05FgQCzEl9rR5owpwtV6kGu7AEuUCklnYXLdYEMlStTJCgWLrSoGxSFHI60gMP5ojFLc1lg1U5ctJCUrYuKgxYMeudsA5ibUSpa2fo6ffb+La5fOIgqJQQ5POVbA2AWxe3/ADEKtWORRFtz/CFEMPI1D7QVllppj6jWcpMtLsVXucEgJPQB8fq7TaWfUEomoKUPQAXqD2CkMAxwbG7dniLSawljMS3yEbgguyn62g6at7OQwLHuzg98Qqbi9jIincGMhI/0s9Uu7hyVJFAZmYgublwCeuG1nKI8OYZpTNLAprKEGYrooOWcEsbY8o10+omEVuxJCQlaQQbspxuOYW7QRJMlaUsgAGoFKgRSsuSUpJLO5YOzE72FtWpW+TAcyfMNXKUTBY1BpdyQ4bqzt3zHms0I1AZaUFhYvfuxKQobG2WDtDKZNSkfxg3Cb3FVhcm9ntg+UQ6iUZhK6nBuwDKBubEE7jpmF8RXa2ZmrK5qvg4FhLXSWZluQblzUANjin8I6uAZ/wAJTUpWoqSwBKWPzAM3k9/bvFslpUkMxA7ErZmSHqfr5QUieSz8wNTbAMzYVuD9YouszR72ScIlEX8LzqmDEO1Q2FVLkfX1jfR/Da1HmNKWVcg/hXQzecXmczKw74FmG32z/LEYnC4PzJCSQ2CfvmH+eyNA8NFeT8LBK5fMCQHKSPmZYf0ZTe0Qaf4YJrCiQUkAYYl1Aju7D3i0iY97l267l7jbGPOMmzQAKsGxPQDmfDNZoT5zJ7h0REJ+G0snxHB8O4T1Fie5FQ9o9kcDkC5qBUeUl2IICmtixAftD86lJJzyFlDYWCvtES/EI5RRzM7fhZ6mOf8AEKupyd2NoQtHAgEtSSHYM7C9/Y794ARwGkpUUqWxwzDNgXLn0G+YtIWWta4HZzfy6e8aV1Ek3Y2F8s3qIX5rIu5tCAdFwwIamXSQ5Llx6F2bffeCUJLAFId+Uju4BIyD+L3iaRUwByRcA7n5g/TPvEtytwkZLO4FmYvuWKrRGU5PdjKCBBKUTdwA58tvePZ6U2CsgAN3U2+w/tEa9QoUMk71jsxY2/pHnBPhJUhVQdTt5AEv6sMmNb7mUQYynFV7OBttHi5Qu93y+bj7f2jzUaxIUEpA5mDZawjWXNKkF/4gB5C794Gp8mI1adJIN3uzmzGxt6j2jedoZYKltzGmrLcoPXzb0jZS1KcYpI/qsCw9D9Iye7uN1B3bD4Y9iYGt+5qFQ4PISukJFQdRDvlwxHSCk6FOwSMDAuOYn7H3gw6dNQLklRIcgFwR12iCdqwElRwD+vv9YLyyfcFE8s8oty+VyAWY/cecaVBRy1QV68pI/XnAmu4kAgF7MVZywYP6/lAUvjkjw0zFqNRDFI2d0lh0s9+vlBSb3oaxlpbEF7Cwtk4v239o3CEpZxzJ9rgi/vCjVfEkvwUKQ1Ux6g3yUkEg+YNoH4txFwUu75byd/eGWOTe4o1malSQXw4xs7sPp94yFcxE/UpAqSlHKoNzKsCPw7Fyb4PrGQ2mHdhKDLlKNwCWv9h+YixSETJqpQJCeRKAVC9yQmwNz6vCGStjkdMOPQGJJmrUTcsGHbAYW6tv/mO632AWn4gXLRp5UlBQpSVKJIsHzv1qG+w7QJwjRCd4gDppQVEDJAawfzEV6ShSlBASS7MGJd8EdfOOi/B8oiWpKpRTy4KSHCnuatyEAbWIcRGcnCJqsF0WgIQUqnMSAwLEOQWenb19YcaPTB5alquhLtbLEKDgkbjc3G8Vnia1ypqZi0Hw3KAACByEJZ8G/wBQ0WbTKEyRVLfJSD17hzuSbRz5NVJvhmQXpyBLy5UCLm5cDr9u0B8UIRpUHNTuHf5k8oDbAkexgSRwjUKWlRmUs6gm7hRdk56kejiGchIaTLUaiPmYMksm7OH/ADsYn5U7TsNGmnWmYkJIA8MIKgHszFvKx9CIKKFqCWSxASTawNgoMcWWWHqYD4QHZTEFSmU4YkIYA3GP7w3qUHUrINLAvfpYZtAk6YUtivp1wKkygmwWGPUUlTnzpIz/AILnrCWByVBQ6ciQSG7hx6CI5+hDcuyncEPyqUWvZiVkdhE8lJKk1SnUKlvsCEAdbZZmDtDOUeUKFGUlRAIBTQQdstftePFqCTS2ByhnFkh7/nEC0qIVkOhDjoTUDfzA9oyVKHirIUl3UCNwFJAZvMA/1QgQiYCAR0DgAlyB93t6xIA1Kai4AB7gvn6e0QmYAoqqZkFPssEFmyzj2jWdM50qdNIBKrP+IEejP6wOTUglYatWAzuBskWfbJMDTQPErA5gooVazEA3+h+kRT9UxRMExkOVMR8woNId+t4zT6ldLLcEVqP9JUH7jti8FJpWCgmWogFaksbnvk5fzT7xpM04KBX+IMR3UCCDc2NVojOrM2YpDsUKCjlighJF+pLj03iHXIP7xQLpuB5Ui3o31MGnYaC5a0JQS/NNALs5LhKW6/8AMSzDzBVXLzClsmql+wBH1hDqOIkGWThRSBYG7EED3f1EeJ4stRT1SolhYXU7frtDeFJ7gssU2YlKXuxZ97izje7CINbqEJmfOwCrgDIUCA58xntCCfxWYkqGQym7XF/PEK53EpqpaiRm9W7lg31gxwSZnItuq1ktNKiSkJze7DY5Jdr/AKMRaXiTqKpakqFQGbiq5dz3H22ika6bMU9WwZvIn/MQJBChkB2J2dvuw+kXXSquRdRdtRxGUJzrWXApJflIUpzYdCAOzxDq/iNArIuXYEM2xs2zCEP/AE5XISLKACe5U7fYmB+K6YoVLlJvUbAbkW84CwwbSYbY34hxMDwpiWIXdiHsCUu2NoNmceQmQFpuSQz25lJKvOzfWBFcGSlAQSVEAjzq/IEvCnXcLUhLtk/di3/1AUcc6RrYdrvifdIc98YtbzhTO4/NVVfLW79v1tAeokMxO94hKPb/ABHRDDjXCBbHGn+I5qVAkuAGbtA+v4wuYABYC7Dcs0BKSBc9IHCoZYoXaRrYXN1ilSwkmwDfV/7wEtWY9Usx4A4LRSKSMYhUSKmls9o8BHT/ABHkxL4H6MYw64dMlIR8ylKLFXMJaQWxcKKiL3YRkRyeGyyTR4k3AZilmySW62Atg5zGRDSnvY6sVcdQBMDJSAxskBIx0EB6AOQTcghveMjIePpMi/cMSE6iWwD0gOwdqFKao3ZwPQAYEWXWKJUtLlmBZyN+0ZGR50/WgrgRr1i0hdKm5KtmdiXbzEFy5hTpCtNlUKW4AHM6btiMjIq0jGvw5qVqUkKUTjNzepy+XsIO0coJnoSHZpimckO46nufePIyOfLtJ/Yy4FGsnKTqZaQSAZqQR1q1QSfoo/oCJJs5X+pAe3g1/wBRrD+0ZGRVLZfZi9hujTJMkAhwTMySd1Hcwv1qyJ0pibzEg3ODNAI9jGRkJj3lv/vIXyMNJzIWTfmWPQAkD6xglJTNmkBiZlJ7gAMIyMgS7mYk4OsnULSS4BUQD/8A0TD5MpNRDBikP3dagb+RjIyGzeoVGunkJaWlnAZgbtcjfsYh1yiyTvWU+hSokeVh7RkZC/qYwTw2SkGeQAP+0n0DsPIPEJTzLTtWzdiASIyMjd3/AL7B7AerkJUuQkiwWW9EqIv6CF3CEAqWSPxI+oBP3MZGRe3T+39sD5PddZ2/m/KNdOgf6aVbID9+YxkZFP0r7mNdRJT/APs2/wDG/wD8k/cRprZSRp7AfNKPqWf7n3j2Mgxe6/H9GHGoQPDlncLlB/Uj7Qq1aB/rdP8A7h9ZSn+wjIyJR7/ZhYzSXmrB2Fu0acWSBQkYBTb+oCMjIVG7MqXHyy0jalP1D/cmF6zyv/LHsZHdj9KJmkxLj0jWYGIbvGRkOY8nJDjyjaSkNGRkZ8GPSLGJeHSgqYhJDgqAMZGQkvSzFlmyU+IAQ4owb7997xkZGR5zbNLk/9k=" /></p>`
})
})
app.listen(3000)
Pug Rendering Example
index.pug
doctype html
html
head
title This is Pug
script(src='https://code.jquery.com/jquery.js')
link(href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css', rel='stylesheet')
body
h1 Rendered Pug file!
div
p A message!
p Another message!
div.container
div.row
div.col-sm-3 #{msg}
div.col-sm-3 Your message is ... #{msg}
div.col-sm-6 Your message is ... #{msg2}
div.col-sm-12 !{html}
div.row
ul.col-sm-12
each country in countries
li= country.name + ' -- ' + country.capital
if country.western
div.western This country is in the western hemisphere
div Message of the day: 2+2 = #{2+2}
pugPractice.js
const path = require('path')
const express = require('express');
const app = express();
const helmet = require('helmet');
app.use(helmet());
app.use(express.static('public'))
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.set('view engine', 'pug')
app.set('views', path.join(__dirname, 'practice-views'))
app.get('/about', (req, res, next) => {
res.render('about', {})
})
function validateUser(req, res, next) {
res.locals.validated = true;
next();
}
app.get('/', validateUser);
app.get('/', (req, res, next) => {
// the data, in the 2nd arg, is going to be appened to res.locals
res.render("index", {
countries: [
{
name: 'Russia',
capital: 'Moscow',
western: false
},
{
name: 'England',
capital: 'London',
western: true
}
],
msg: "Failure!",
msg2: "Success!",
// HTML came from the DB and we want to drop it in the template
html: `<p><img src="data:image/jpeg;base64,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" /></p>`
})
})
app.listen(3000)
Request and Response Objects: Revisited
Forms: getting data from the request
object
Consider the following basic login.ejs
file with a basic form:
<link rel="stylesheet" type="text/css" href="/stylesheets/styles.css">
<div class="login-page">
<div class="form">
<form action="/process_login" method="post" class="login-form">
<input type="text" placeholder="username" name="username" />
<input type="password" placeholder="password" name="password"/>
<button>login</button>
<p class="message">Not registered? <a href="#">Create an account</a></p>
</form>
</div>
</div>
The action
attribute on a form
tag determines where the form is going to be submitted when it is submitted (i.e., what endpoint relative to the host). On a front-end framework, you would never submit the form because you don't want to leave the HTML page (see this post about how the preventDefault
method is often used on events in the browser to stop forms from automatically submitting when the submit button is clicked). You would use preventDefault
when using a front-end framework like React; this gives you the chance to instead submit the form data asynchronously using JS to make an AJAX request using axios
or another HTTP client. Specifically, when using Express for server-side rendering instead of building APIs, you have a route that specifically handles the data submitted for certain types of HTTP requests whereas in something like React you will need to transmit the data submitted on the front-end using an HTTP client like axios
. But in our case right now, where we're using Express in the context of server-side rendering, we will use res.render
and Express will be in charge of rendering every single page by means of a view engine. So we are going to have Express move the form once it is submitted on to the /process_login
route, and it will be a post
request (the method
attribute on the form indicates what kind of request is going to be fired off once the form is submitted).
Everything described above should immediately remind us that we need some middleware in Express to handle what happens when we get a request, specifically a post
request, to the /process_login
route. For right now let's just do a simple test:
app.post('/process_login', (req, res, next) => {
console.log(req.body); // see what data is coming across the wire from the form
res.json({
message: 'You tried to login!'
})
})
NOTE: Redirects in post routes |
---|
It is worth noting here that the browser never sees what goes on at /process_login . Think about what typically happens when you try to log in to your Amazon account, Gmail, etc. Are you not redirected to a page that indicates success (e.g., a dashboard, your posts feed, etc.) or a page that indicates failure (e.g., maybe being redirected to the same page but now with an error message displayed)? The point is that once you submit the form with your credentials/information, this information goes to the server (our Express server!), and it is now up to us to figure out what to do with the user's request in terms of how we respond.If a request comes in from a user posting data by means of a form submission, then the response will almost always boil down to first doing something with the data behind the scenes in Express (e.g., pulling from the database and matching/validating user credentials, etc.) and then redirecting the user accordingly. In Express, the way to redirect a user is with res.redirect([status], path) , a method touched on later in these notes.Of course, it is also worth noting that the method on a form can be not only post but also put (to update/edit things like comments, blog posts, etc.), get (retrieve certain information), etc.In the case of server-side rendering (when you use a view engine(s) and the like), you will often handle form submissions from a user and redirect them internally within Express before landing on a route where you render a view to the user, something like login.ejs or dashboard.ejs or whatever view it might be. But you handle the logic internally and the user does not see the logic being hashed out in the browser but sees whatever view you end up responding with (if that's what you choose to ultimately respond with).In the case of single page applications (e.g., when using something like React), you will often use Express to build an API and you will make routes that respond with JSON. In this way, you can design your UI so that you have routing on the front-end that directs your user to different pages/routes in the browser (using React Router, for example), but you can respond with JSON from Express (probably from your database) based on what routes the user hits or what requests they issue on the front-end. As an example, if you're using React for your front-end and Node, Express, and Postgres for your backend, then you may use react-router and react-router-dom for a lot of your routing on the front-end. Routing on the front-end will often mean rendering different components based on what front-end routes are hit. It's within the components that you want rendered based on the front-end routing that you make asynchronous calls to your backend. As a simple example, suppose a coordinator went to /dashboard from your root domain in the browser. Then you may choose to render several components, where the main one might be something like <CoordinatorDashboard /> . Inside this component, you might use an HTTP client like axios to make an HTTP request (to the appropriate route with the appropriate method) to wherever your Express server is listening. Your Express server would then respond with JSON (from you database most likely) which you would then use to populate meaningful things in the <CoordinatorDashboard /> component. |
The data, as it is being passed from the form
, is coming from two input
boxes, with type
of text
and password
, being submitted by the user. In HTML, whatever the name
attribute is set as is what is going to be passed to whomever comes next (i.e., user-submitted information will come across the wire as key-value pairs where the key is determined by what we specify for name
on our input
and the value is whatever the user submits).
In our case, our /process_login
route is going to get the submitted data through body
on the request
object (thanks to our app.use(urlencoded({ extended: false }))
middleware) and the property names will correspond to whatever name
s were set on the input
tags with the property values being whatever the user entered.
For example, suppose our form
had the following input
tags:
<input type="text" placeholder="username" name="some" />
<input type="text" placeholder="user" name="thing" />
<input type="password" placeholder="password" name="else"/>
And suppose the user typed in values of An
, illustrative
, example
into the different input
fields, respectively. Once the form
was submitted, we we would see something like the following come across the wire on req.body
:
{
some: 'An',
thing: 'illustrative',
else: 'example'
}
Whatever the user submitted will come through the HTTP message and the form will come through as urlencoded
(hence the need of the Express middleware to parse the HTTP message corresponding to the user-submitted request and to tack on the data to the req.body
object). Note that before the urlencoded
request gets to app.post('/process_login', ...)
it is subjected to all of the middleware used at the application level, namely app.use(express.urlencoded({ extended: false }))
. This middleware parses the request
object coming from the user and will add to the request
object a body
property which will have the user-submitted data from the form on it.
What we actually want to accomplish here is we want to decide what to do with the user. Think about what happens when you log in to a site. Are you redirected to a certain page only for logged in users? Are you redirected to your dashboard? Somewhere else? The point is we can have a bunch of logic in our routes to handle the user effectively. And almost always your logic will depend on properties on the request
object. In the case of form-submitted data, we have access to information on the body
of the request
which should further inform what we want to do with the user. It's fairly common to destructure information off the body
of the request
like so (as an example of our login form):
const { username, password } = req.body;
Then you can do stuff like check the database to see if a user's credentials are valid (maybe you're using bcrypt
or some version of blowfish
or some sort of algorithm or OAuth). As an example, suppose we want to direct the user to the welcome page if they are valid. And we might want to save their username in a cookie--or you could use sessions (we want to do this to make it readily available).
Sessions and cookies are very similar. The difference is that cookie data is stored entirely on the browser and the browser will send it up to the server every time a request is made. Session data, on the other hand, is stored on the server and the browser is essentially given a key for that data. But sessions are not included with Express (you can use express-session
if you want to use that instead of cookies). But the ability to do things with cookies is built-in with Express so we'll just use that instead in this case. You could use local data too, but we're just going to stick with cookies for this.
Cookies: using res.cookie
, res.clearCookie
, and other cookie-related information
As always, it is best to look at the docs. We see the following for res.cookie(name, value [, options])
: Sets cookie name
to value
. The value
parameter may be a string or object converted to JSON. The options
parameter is an object that can have the following properties.
Property | Type | Description |
---|---|---|
domain | String | Domain name for the cookie. Defaults to the domain name of the app. |
encode | Function | A synchronous function used for cookie value encoding. Defaults to encodeURIComponent . |
expires | Date | Expiry date of the cookie in GMT. If not specified or set to 0, creates a session cookie. |
httpOnly | Boolean | Flags the cookie to be accessible only by the web server. |
maxAge | Number | Convenient option for setting the expiry time relative to the current time in milliseconds. |
path | String | Path for the cookie. Defaults to "/" . |
secure | Boolean | Marks the cookie to be used with HTTPS only. |
signed | Boolean | Indicates if the cookie should be signed. |
sameSite | Boolean or String | Value of the "SameSite" Set-Cookie attribute. More information here |
Note: All res.cookie()
does is set the HTTP Set-Cookie
header with the options provided. Any option not specified defaults to the value stated in RFC 6265.
See the res.cookie
docs for example usage (and also note you can set multiple cookies in a single response by calling res.cookie
multiple times).
For our uses right now, we'll just note that res.cookie
takes at least 2 arguments:
- The
name
of the cookie. - The
value
to set it to.
So every time whoever the response is sent to makes a request, they're going to send their cookie up so the server will have all of that data available to it. So the example of using the form to submit a username and password and subsequently getting the values off req.body
const { username, password } = req.body;
can now be used inside of our route in a conditional way to set a cookie:
app.post('/process_login', (req, res, next) => {
const { username, password } = req.body;
if (password === 'x') {
res.cookie('username', username);
res.redirect(303, '/welcome');
}
res.json(req.body);
})
Once the form is submitted, the application-level middleware app.use(express.urlencoded({ extended: false }))
parses the HTTP message and tacks the data onto a body
object which is appended to the request
object before our route middleware app.post('/process_login', ...)
deals with the request
. If the password supplied has a value of 'x'
, then we will stash the username
in a cookie so that going forward we can access that username value on any page and we don't need to remember it (the cookie is stored on the user's browser and thus we do not need to find out the cookie info for each new request ... we are simply using what we stored on the user's browser in the form of a cookie). Why does this matter? Well, if the user comes back or goes to a new path, then we will not have access to req.body
anymore. We get a totally new response and a totally new request. Remember that's part of HTTP. It's stateless. There's no dialogue going on. It's just the one-off process. You get one request and you get one response. And then we start completely over.
So that is what the cookie is for--cookies allow us to persist data from one request (e.g., username, preferences, etc.) to numerous requests by storing information on the user's browser. This information (in the form of cookies) is sent up to the server by the browser for every subsequent request, thus allowing us access to information stored in cookies that would otherwise have to be left behind due to the stateless nature of HTTP. See the comments at the end of this note for more about this.
Once we've stashed the username
in the cookie, we can call res.redirect
to send the user where we want them to go. From the docs for res.redirect([status,] path)
: Redirects to the URL derived from the specified path
, with specified status
, a positive integer that corresponds to an HTTP status code . If not specified, status
defaults to "302 Found". (See the docs for more detail and examples.)
The essential fact is that res.redirect
takes one argument (unless we give it the optional first one as a status code as specified above): where to send the browser. We shall, for the time being, simply have res.redirect('/welcome')
. So to recap:
app.post('/process_login', (req, res, next) => {
const { username, password } = req.body;
if (password === 'x') {
res.cookie('username', username);
res.redirect('/welcome');
}
})
When the user submits the form, if their password is x
, then we will store their username in a cookie with name
of username
with value of username
. We will then redirect the user to the /welcome
route. We can add some more conditional logic to ensure they are redirected somewhere else (programmatically instead of manually trying to move around using the URL) if their password is not x
:
app.post('/process_login', (req, res, next) => {
const { username, password } = req.body;
if (password === 'x') {
res.cookie('username', username);
res.redirect('/welcome');
} else {
res.redirect('/login?msg=fail');
}
})
So if the user did not submit the form with a password of x
then we will send them right back to the /login
page but we're going to put in a little query string ?msg=fail
as well (using query strings in Express is discussed in detail in another note).
To review at a higher level: The /process_login
is a post
route and note that the user will never see what goes on here. The browser will never see what is happening behind the scenes (i.e., inside of Express). The user will come here (i.e., to this route) as soon as they submit the form, but there is no res.render
, res.send
, or res.json
. There are only res.redirect
constructs. This means the user will hit this route after submitting the form just long enough to check if the password is x
or not and then we'll redirect them to either /welcome
or back to /login
(with the query string ?msg=fail
). Both redirects will result in GET
requests to either /welcome
or /login
.
NOTE: res.redirect and the optional first argument of an HTTP status code |
---|
Super short version: 302 (the default value) or setting 303 manually will result in a GET request to the specified route while setting 307 will result in the HTTP method being handled on the current route being used for the route you are redirecting to (i.e., setting 307 will result in POST -> POST , PUT -> PUT , GET -> GET , etc.).Longer version: your redirect will either be a GET request (due to using the default value of 302 for the optional first argument to res.redirect as an HTTP status code or manually specifying 303 ) OR the same kind of request for whatever request you are currently handling by specifying 307 for the optional status value (i.e., using res.redirect(307, '/something') from within a HTTP METHOD request to a certain route (e.g., PUT , POST , etc.) will result in another HTTP request of the same METHOD to another specified route).As a simple example, if we are handling app.<METHOD>('/login', ...) and somewhere in ... we want to redirect the user to /welcome using res.redirect(status, '\welcome') , then we cannot set status at all (in which case 302 will be used by default and a subsequent GET request will be issued to the /welcome route), we can set status to 303 which will more explicitly ensure we issue a GET request to '/welcome' , or finally we can set status to 307 which will explicitly ensure we issue a METHOD request to '/welcome' (i.e., the same kind of request we were presently handling). Of course, other status values can be used, but these will typically be the ones you want to use. Check out the HTTP Status Code site for more info and first note that 3xx relates to redirections. |
The point of all of the above is to note that there's no real "option" for the user in the /process_login
route we've created above. This post
route's only purpose is for the user to submit data and then for us to decide what to do with the user (i.e., a redirection of some sort or something else) based on that data. Remember that's what a post
route is for: It means we want to submit some data (i.e., we want to accept some data from the user and then send them on to where they belong).
Let's revisit the code we currently have:
app.post('/process_login', (req, res, next) => {
const { username, password } = req.body;
if (password === 'x') {
res.cookie('username', username);
res.redirect('/welcome');
} else {
res.redirect('/login?msg=fail');
}
})
If the user has password x
and is redirected to /welcome
using a GET
request, then we need to set up some middleware to handle the GET
request at the /welcome
route. Here's the welcome.ejs
view:
<link rel="stylesheet" type="text/css" href="/stylesheets/styles.css">
<div class="login-page">
<div class="form">
<h1>Welcome back to the site, <%= username %>!</h1>
<p><a href="/logout">Log out</a></p>
</div>
</div>
If we want to put the actual username
of the user in the DOM, then we need to be able to access it. Well where is the username
for us to access it? We stored it in a cookie when handling the POST
request to the process_login
route (note that the username
is made available to us upon submission of the form via req.body
):
app.post('/process_login', (req, res, next) => {
const { username, password } = req.body;
if (password === 'x') {
res.cookie('username', username);
res.redirect(303, '/welcome');
} else {
res.redirect('/login?msg=fail');
}
})
This presents an interesting issue for us when setting up the middleware for GET
requests to /welcome
:
app.get('/welcome', (req, res, next) => {
res.render('welcome', {
username: req.body.username
});
})
The above route handler will fail as it is currently written (i.e., username
will not have a meaningful value since req.body.username
will be undefined). Why? Well, there's not actually any username
on req.body
at this point. Why? Because a new GET
request was made to /welcome
where the username
is no longer available! That is, username
is not being sent through the body
of the request
object anymore--that happened on the first request. As just noted, the GET
request to /welcome
coming from the redirect is a totally new request now. There is no username
to access! This is why we stored the username
in a cookie (on the user's browser) when username
actually was available to us on the request body:
const { username, password } = req.body;
...
res.cookie('username', username);
...
So now instead of sending req.body.username
, which is empty, we will instead send req.cookies.username
:
app.get('/welcome', (req, res, next) => {
res.render('welcome', {
username: req.cookies.username
});
})
So the req.cookies
object will have a property for every named cookie that has been set. The reason we have a req.cookies.username
to access is because of setting it previously with res.cookie('username', username);
. Note that you set cookies in a singular fashion (i.e., res.cookie(name, value)
) while they are available in a plural fashion as key-value pairs on the req.cookies
object.
Before we actually start using information from the cookies we set, we will need a third-party module to parse the Cookie
header from the HTTP message (much the same as we needed app.use(express.json())
and app.use(express.urlencoded({ extended: false }))
to help parse the body of a request after a form submission so we could easily grab the user-submitted data ... we cannot use this middleware here because the cookies are coming across in the Cookie
header of the HTTP message and not the body). We will use the cookie-parser
package (by first doing npm i cookie-parser
and then putting const cookieParser = require('cookie-parser');
and app.use(cookieParser());
in our application code) which states the following at the top of its documentation:
Parse Cookie
header and populate req.cookies
with an object keyed by the cookie names. Optionally you may enable signed cookie support by passing a secret
string, which assigns req.secret
so it may be used by other middleware.
From our welcome.ejs
view we will include the ability for the user to logout if they so desire:
<p><a href="/logout">Log out</a></p>
Now we need to make a route for /logout
, specifically one that accounts for a GET
request because an anchor tag always points to a GET
route. When the user logs out, we will want to clear their username
cookie using res.clearCookie(name [, options])
.
NOTE: using res.clearCookie(name [, options]) to clear cookies |
---|
Clears the cookie specified by name . For details about the options object, see res.cookie() .Web browsers and other compliant clients will only clear the cookie if the given options object is identical to those given to res.cookie() , excluding expires and maxAge .Example: res.cookie('name', 'tobi', { path: '/admin' }) and res.clearCookie('name', { path: '/admin' }) . |
In our case, we will simply want to clear the username
cookie like so:
res.clearCookie('username')
Note that you can only clear cookies individually by name. If you wanted to clear all cookies in one go, you could do something like the following:
for (let property in req.cookies) {
res.clearCookie(property)
}
Further still, if you only wanted to clear certain kinds of cookies that you had set, then you could add some conditional logic to specify that as well.
It is essential to note the simplified sort of "lifecycle" of a cookie, especially in the context of working within Express. Essentially, you can set a cookie in Express using res.cookie(name, value [, options])
. The res
in res.cookie
indicates you are attaching something (i.e., a cookie) to the response object and eventually you will send that cookie back to the user's browser when you send the full response with res.send
, res.json
, res.redirect
, etc.
You can see the cookies set on the browser by hopping to the browser console and entering document.cookie
. Alternatively, and more effectively, you can open the dev tools and navigate to the "Application" tab and click on "Cookies" in the menu pane under "Storage". Choose http://localhost:3000
as the source:
Now suppose our /login
, /process_login
, /welcome
, and /logout
routes were written as follows:
app.get('/login', (req, res, next) => {
console.log('Cookies available at /login:', req.cookies);
res.render('login', {msg: ''});
})
app.post('/process_login', (req, res, next) => {
const { username, password } = req.body;
if (password === 'x') {
res.cookie('username', username);
res.redirect(303, '/welcome');
} else {
res.redirect(303, '/login?msg=fail');
}
})
app.get('/welcome', (req, res, next) => {
res.render('welcome', req.cookies);
})
app.get('/logout', (req, res, next) => {
console.log('Cookies before clearing:', req.cookies);
res.clearCookie('username');
console.log('Cookies after clearing:', req.cookies);
res.redirect(303, '/login');
console.log('Cookies after clearing and after redirect:', req.cookies);
})
If we started at http://localhost:3000
, navigated to http://localhost:3000/login
, logged in with username
as Bill
and password
as x
, and subsequently logged out, then we would see the following in the browser console:
In the Node console, however, we would see the following:
Cookies available at /login: [Object: null prototype] {}
Cookies before clearing: { username: 'Bill' }
Cookies after clearing: { username: 'Bill' }
Cookies after clearing and after redirect: { username: 'Bill' }
Cookies available at /login: [Object: null prototype] {}
Let's walk through what happens for each line above (a more concrete verbal explanation follows):
- There are no cookies available when we first hit the
/login
route because no cookies have been set. We now log in with "Bill" as the username and "x" as the password. When we submit the form, we are making aPOST
request that is processed by the/process_login
route. Since the password was "x", we executeres.cookie('username', username)
, and then we redirect the user to the/welcome
route. We cannot access the username by means ofreq.body.username
on the/welcome
route due to the stateless nature of HTTP requests, but we can still access the username by means ofreq.cookies
. - Before using
res.clearCookie('username');
we logreq.cookies
and see{ username: 'Bill' }
is present. - After using
res.clearCookie('username');
we logreq.cookies
and see{ username: 'Bill' }
is still present. - After using
res.clearCookie('username');
andres.redirect(303, '/login');
we logreq.cookies
and see{ username: 'Bill' }
is still present. - After using
res.clearCookie('username');
andres.redirect(303, '/login');
, we are now at the/login
route, and loggingreq.cookies
gives us an empty object{}
.
Why does the value for req.cookies
make sense at each of the steps delineated above? The reason why we see everything above is because everything that has to do with setting or clearing cookies happens to the response object via res.cookie
and res.clearCookie
, respectively. When the browser submits a request to the server, it sends up whatever cookies it has stored in the Cookie
header and the body-parser
middleware parses this header and populates req.cookies
with an object keyed by the cookie names.
Since res.cookie
and res.clearCookie
only effect what we res
pond with, it stands to reason res.clearCookie
has no impact on req.cookies
for however long we have access to the original request (i.e., while we are inside of the route handler/callback). This is why { username: 'Bill' }
is still present when logging req.cookies
for steps 3 and 4 described above.
Once a new request is received by the server, the whole process starts over, and on req.cookies
we receive whatever was sent over by the browser by means of the Cookie
header (and parsed by cookie-parser
). Since res.clearCookie
results in the browser removing whatever cookies (in this case the username
cookie) we have specified upon receiving the res
ponse we have packaged together and sent back, when the browser sends up the new request the removed cookie will no longer be present in the Cookie
header, and this will be reflected when we log req.cookies
for this new request where the Cookie
header has changed. It may be clearer to consider key points concerning req
and res
for each route we hit in the process outlined above:
GET
|/login
:req
: TheGET
request made to the/login
page by the browser is nothing special. No cookies are available to the browser at this point since none have been set. Thus,req.cookies
at this point is simply{}
(i.e.,cookie-parser
gives us an empty object since no cookies are present from theCookie
header).res
: We respond with thelogin.ejs
view, and this view contains the form the user will fill out.
POST
|/process_login
:req
: Once the user has filled out the form made available inlogin.ejs
view and submitted the form, we get aPOST
request to the/process_login
route. We will first executeconst { username, password } = req.body;
, where theusername
andpassword
information are made available to us onreq.body
by means of theapp.use(express.urlencoded({extended: false}));
middleware.res
: Since the user has provided a username of "Bill" and a password of "x", we start preparing the response we plan to send back to the browser by executingres.cookie('username', username);
. This attaches information to the response by means of modifying theCookie
header. We then executeres.redirect(303, '/welcome');
. The303
status as the first argument tores.redirect
indicates we will be making aGET
request to the route we are being redirected to,/welcome
(as specified in the second argument tores.redirect
). Once the browser receives our response, the cookie may be seen as previously illustrated.
GET
|/welcome
:req
: The browser issues aGET
request to the/welcome
route, but there is now meaningful data in this request'sCookie
header. Specifically,cookie-parser
will ensure{ username: 'Bill' }
is now available onreq.cookies
.res
: We respond by not only rendering thewelcome.ejs
view, but we pass this view local data, namelyreq.cookies
. So everything onreq.cookies
will be made immediately available inside of thewelcome.ejs
view. This is why we are able to use<h1>Welcome back to the site, <%= username %>!</h1>
inside ofwelcome.ejs
.
GET
|/logout
:req
: We click the "Log out" button in thewelcome.ejs
view which is coded as follows:<p><a href="/logout">Log out</a></p>
. Recall that anyhref
for ana
tag results in aGET
request to the specified location. Hence, when we click the "Log out" button we will be making aGET
request to the/logout
route. Again, the request sent to this route by the browser has our cookie as part of theCookie
header. Ourcookie-parser
middleware does its work and makes sure{ username: 'Bill' }
is available onreq.cookies
, as made evident when we logreq.cookies
to the console.res
: The main purpose of the/logout
route, as it is currently written, is simply to clear theusername
cookie and then redirect the user to the/login
route. Hence, we clear theusername
cookie from the response we are packaging together to send back to the browser by executingres.clearCookie('username');
. We then redirect the user to make aGET
request to the/login
route by executingres.redirect(303, '/login');
. The response we send back no longer has ausername
cookie per our execution ofres.clearCookie('username');
.
GET
|/login
:req
: This is the same as how we started; that is, the browser does not send up any cookies or any data in theCookie
header.res
: We simply respond with thelogin.ejs
view much as we did before.
In conclusion, it is good practice to clear unnecessary cookies. It is good to get it out of the user's system so you are not clogging it up and so that sensitive data is not available later on. The really nice thing about cookies is that they give us a way to persist user-specific information across different requests despite HTTP being stateless.
Redirects: the optional status
value in res.redirect([status,] path)
First, take a look at the Express docs for res.redirect
. Second, realize that HTTP status codes are actually quite important. Third, note that we should really probably always specify the status
value to most likely be 303
(when we want our redirect to issue a GET
request) or 307
(when we want our redirect to use the same method request as was being handled before the redirect). Of course, other status values can be used, but 303
and 307
will be the most common.
Take a look at this awesome answer on Stack Overflow. Some of the highlights:
- The
303 "See Other"
redirect should always be followed by aGET
(orHEAD
) request (according to the HTTP/1.1 spec), notPUT
or anything else. See RFC 7231, Section 6.4.4: - The other popular types of redirects -
301 "Moved Permanently"
and302 "Found"
in practice usually work contrary to the spec as if they were303 "See Other"
and so aGET
request is made. From this answer (noting how302
statuses were originally implemented incorrectly by browsers so codes303
and307
largely exist for this reason): For historical reasons, a user agent MAY change the request method fromPOST
toGET
for the subsequent request. If this behavior is undesired, the 307 (Temporary Redirect) status code can be used instead. - Excerpt from Wikipedia: This [i.e., what's touched on in the point above] is an example of industry practice contradicting the standard. The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect (the original describing phrase was "Moved Temporarily"), but popular browsers implemented
302
with the functionality of a303 See Other
. Therefore, HTTP/1.1 added status codes303
and307
to distinguish between the two behaviours. However, some Web applications and frameworks use the302
status code as if it were the303
. - There is a
307 Temporary Redirect
(since HTTP/1.1) but it explicitly disallows changing of the HTTP method, so you can only redirect aPOST
toPOST
, aPUT
toPUT
, etc., which can sometimes be useful.
Long story short: It's a good idea to explicitly pass the status
value to res.redirect
even though it is optional, and you should pass 303
if you want your redirect to issue a GET
request or 307
if you want your redirect to issue the same kind of request currently being handled by the redirect.
Passing data: the query string (using req.query
)
Recall how we handled a POST
request to the process_login
route:
app.post('/process_login', (req, res, next) => {
const { username, password } = req.body;
if (password === 'x') {
res.cookie('username', username);
res.redirect(303, '/welcome');
} else {
res.redirect(303, '/login?msg=fail');
}
})
We've explored in detail what happens when the password is x
, but what happens when the password isn't x
? It's clear we redirect the user back to the /login
route using a GET
request, but we tack something else onto the route: ?msg=fail
. What's the deal with the ?
?
The ?
is a special character in a URL. The ?
is almost like a delimiter in a URL that says, "Every part after me is a part of the query string. Everything before me is part of the actual path, the domain, the protocol, etc. So the web server will stop caring period from ?
onward. The ?
denotes the beginning of the query string and then you have key-value pairs after it. If you want to have more than one key-value pair (we only have one with msg=fail
), then we can use an ampersand (i.e., &
) to denote another key-value pair in the query string. For example, we could have http://localhost:3000/login?msg=fail&consolation=boohoo
. So the keys here are msg
and consolation
while their values are fail
and boohoo
, respectively.
Definitely read the Wiki entry on query strings. It's useful to know their history, background, and usage. Also make sure to read the docs on req.query
(key portions included below because of some of the useful examples the docs include):
From the docs: This property (i.e., req.query
) is an object containing a property for each query string parameter in the route. When the query parser property is set to disabled
, it is an empty object {}
; otherwise, it is the result of the configured query parser.
Note: As req.query
's shape is based on user-controlled input, all properties and values in this object are untrusted and should be validated before trusting. For example, req.query.foo.toString()
may fail in multiple ways, for example foo
may not be there or may not be a string, and toString
may not be a function and instead a string or other user-input.
Examples:
// GET /search?q=tobi+ferret
console.dir(req.query.q)
// => 'tobi ferret'
// GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse
console.dir(req.query.order)
// => 'desc'
console.dir(req.query.shoe.color)
// => 'blue'
console.dir(req.query.shoe.type)
// => 'converse'
// GET /shoes?color[]=blue&color[]=black&color[]=red
console.dir(req.query.color)
// => ['blue', 'black', 'red']
We should note that you can build fairly complicated query strings if you so desire, but that shouldn't be your goal. As an example, consider the following needlessly complicated query string (broken across &
on new lines to make it clearer ... note that what follows would not be a valid query string because of the newlines introduced):
?typical=example
&some+space=not+so+common
&myFriends=Oscar
&myFriends=Andy
&myFriends=Angela
&myShoe[color]=brown
&myShoe[type]=dress
&myShoe[brand]=Gucci
&myNested[firstNest]=littleNest
&myNested[secondNest][unnecessary]=true
&myNested[secondNest][useful]=probably+not
We can untangle what is shown above and comment on key aspects:
?
# Typical use case
typical=example
# Include some spaces (more typical of values than keys)
&some+space=not+so+common
# Build a myFriends array
&myFriends=Oscar
&myFriends=Andy
&myFriends=Angela
# Build a myShoe object
&myShoe[color]=brown
&myShoe[type]=dress
&myShoe[brand]=Gucci
# Build an object with an object inside it
&myNested[firstNest]=littleNest
&myNested[secondNest][unnecessary]=true
&myNested[secondNest][useful]=probably+not
The "real" query string (i.e., without newlines) would be as follows:
?typical=example&some+space=not+so+common&myFriends=Oscar&myFriends=Andy&myFriends=Angela&myShoe[color]=brown&myShoe[type]=dress&myShoe[brand]=Gucci&myNested[firstNest]=littleNest&myNested[secondNest][unnecessary]=true&myNested[secondNest][useful]=probably+not
The result upon parsing the real query string above is the req.query
object:
{
typical: 'example',
'some space': 'not so common',
myFriends: [ 'Oscar', 'Andy', 'Angela' ],
myShoe: { color: 'brown', type: 'dress', brand: 'Gucci' },
myNested: {
firstNest: 'littleNest',
secondNest: { unnecessary: 'true', useful: 'probably not' }
}
}
The point is that the query string is a very common way of passing data around the web. Generally speaking, the query string is where you put insecure data. So you don't care about that data. If someone is watching the HTTP traffic on a route or something like that they'll be able to see everything go through in terms of the path requested and the following query string, but they won't be able to see the body (that will be encrypted so long as you are using HTTPS), but they will be able to see the URLs being passed around. So you would never want to put a password or any sensitive data in the query string.
It's very easy for the browser to pull stuff out of the URL. The browser can't pull stuff out of the HTTP body because that's already happened. But the browser can see the URL so the browser (i.e., meaning front-end JavaScript) can pull data out of the query string if it needs to. The server can pull it out too which is what we are just about to do.
To illustrate some of this, go to Google and submit a search for Udemy
. If you look at the URL after your search is submitted, then you will see something like the following (of course different for you):
https://www.google.com/search?safe=off&source=hp&ei=fEeXXrWqLNeD9PwP8OalmAU&q=Udemy&oq=Udemy&gs_lcp=CgZwc3ktYWIQAzICCAAyBQgAEIMBMgIIADICCAAyAggAMgIIADICCAAyAggAMgUIABCDATIFCAAQgwFKFwgXEhMxMWcxMDRnODlnNzRnMTQyZzc5Sg8IGBILMWcxZzFnMWcxZzFQ0sIDWOvGA2CnyQNoAHAAeAGAAYkBiAGrBJIBAzYuMZgBAKABAaoBB2d3cy13aXqwAQA&sclient=psy-ab&ved=0ahUKEwi12Lu0_eroAhXXAZ0JHXBzCVMQ4dUDCAk&uact=5
And if we break down the query string:
https://www.google.com/search # protocol (HTTPS), subdomain (www), root domain (google), path (search)
? # start of query string
safe=off # maybe filtering out some undesirable results
&source=hp # some internal meaning to Google (probably meaning user searched from home page)
&ei=fEeXXrWqLNeD9PwP8OalmAU # also some internal meaning to Google
&q=Udemy # this is what we care about
&oq=Udemy
&gs_lcp=CgZwc3ktYWIQAzICCAAyBQgAEIMBMgIIADICCAAyAggAMgIIADICCAAyAggAMgUIABCDATIFCAAQgwFKFwgXEhMxMWcxMDRnODlnNzRnMTQyZzc5Sg8IGBILMWcxZzFnMWcxZzFQ0sIDWOvGA2CnyQNoAHAAeAGAAYkBiAGrBJIBAzYuMZgBAKABAaoBB2d3cy13aXqwAQA&sclient=psy-ab&ved=0ahUKEwi12Lu0_eroAhXXAZ0JHXBzCVMQ4dUDCAk&uact=5 # another internal
You could just as well enter the following for our own desired results: https://www.google.com/search?q=Udemy
. All of the other stuff was for Google's internal record-keeping. They were constructing their own URL. And that is precisely what we are doing with res.redirect
. Using something like res.redirect(303, '/login?msg=fail&test=hello');
lets us know what happened (i.e., if the user ended up back at the login page, then we can take action and tack on useful information to the query string).
What could we want to use the query string for? In our case, we are using a view engine, so we are going to want to let the user know that their login failed, but, like in the case of Google, even if this is just an API where you're not using Express as a view engine (e.g., using Express as an API for a single page application using React, Angular, Vue, etc.), you can still use the information from the query string. If inside the query string the msg
property happens to equal fail
, then we will want to let the user know on the screen that something happened.
One sample use case you can bake into your applications is something like the following for managing login attempts/failures. Say we have the following middleware placed above any of our routes in our code (i.e., to ensure it runs before any HTTP requests are handled):
app.use((req, res, next) => {
if(req.query.msg === 'fail') {
res.locals.msg = 'Sorry. This username and password combination does not exist.';
} else {
res.locals.msg = '';
}
next();
})
The above code reflects our desire for the following: If the query string has a msg
property with its value as fail
, then what we want to do is set a local variable that the view engine will be able to see, and the easiest way to do that is to use res.locals
because the view engine has access to that as does every other piece of middleware throughout. So if someone happens to need to know, for any reason, that the user tried to log in and couldn't (e.g., maybe we have a counter somewhere to try to prevent the user from being able to log in more than 3 times, etc.), then we will set msg
on res.locals
to indicate an error if there is a failure to log in and otherwise be set to an empty string.
Note that by using this middleware in this way (i.e., at the application level so it runs whenever any type of request is issued) we do not have to pass msg
as the second argument to res.render
to be made available as a local variable to the view that is rendered (because we always have access to res.locals
via the locals
object in our view).
Passing data: parameters via URL wildcards (using req.params
and app.param()
)
As observed in the previous note, the query string is a great way of passing insensitive data through the URL. Another way of passing insensitive data through the URL is through parameters or sort of like through wildcard pieces of the path itself.
Suppose our welcome.ejs
view looked like the following:
<link rel="stylesheet" type="text/css" href="/stylesheets/styles.css">
<div class="login-page">
<div class="form">
<h1>Welcome back to the site, <%= username %>!</h1>
<a href="/story/1">Story 1</a>
<a href="/story/2">Story 2</a>
<a href="/story/3">Story 3</a>
<p><a href="/logout">Log out</a></p>
</div>
</div>
So we want to give the user some story link options upon logging in. Remember that all anchor tags point toward a GET
request and right now we don't have a route to handle anything going to /story
. In fact, we run into a bit of a problem here because presumably each story
will be roughly similar, but of course we have unique stories themselves. Are we going to write individual route handlers for every individual story? Of course not. Imagine the pain that would go into architecting a site that had hundreds of thousands of different routes for individual items that were all structurally similar (e.g., blog posts):
app.get('/story/1', (req, res, next) => {
res.send(`<h1>Story 1</h1>`)
})
app.get('/story/2', (req, res, next) => {
res.send(`<h1>Story 2</h1>`)
})
app.get('/story/3', (req, res, next) => {
res.send(`<h1>Story 3</h1>`)
})
And that's only with 3 stories! Again, imagine having thousands of stories. Technically, the above code accomplishes what we want. But it stinks. It will be a major headache to manage. It's clogging up our actual routes (three routes that all basically do the same thing), and more to the point, we don't want res.send
for some piddly HTML. We want to use res.render
when using a view engine or res.json
if we're using React, Angular, Vue, or something similar on the other end. So this is not tenable. We're going to make copy/paste errors or we'll need one view that can manage all of it, or we'll want one res.json
to be able to handle all of it. So rather than getting really fancy with some middleware, Express already has something built in to handle this very thing.
Of course, we will certainly have to come up with a route to handle GET
requests to /story
, but we can do so in a way that accounts for individual stories:
app.get('/story/:storyId', (req, res, next) => {
res.send(`<h1>Story 1</h1>`)
})
In a route, anytime a thing has a :
in front of it means that thing is a wildcard. In the case above, storyId
is the wildcard, and storyId
(or whatever you choose to name the wildcard) will match anything in the slot following the :
(unless you append a regular expression following the wildcard name in parentheses which is addressed later in this note--you would do this to restrict what kind of wildcard matching you want whether it be only digits, certain kinds of strings, etc.; if a regular expression is added onto a wildcard and the match fails then nothing will be added on the req.params
object). What this means is that the route above will trigger if the user goes to /story/<anything-else>
; that is, it will trigger on story/1
, story/2
, story/3
, and even /story/hubbadubbabubbub
. Anything in the wildcard spot will be matched--we don't care what it is. We just care that they went to /story
followed by something else.
So how do we access the wildcard information? The req.params
object always exists (its default value is {}
). As always, visit the docs for req.params
to find out more.
From the docs: This property (i.e., req.params
) is an object containing properties mapped to the named route "parameters". For example, if you have the route /user/:name
, then the "name" property is available as req.params.name
. The req.query
object defaults to {}
.
// GET /user/tj
console.dir(req.params.name)
// => 'tj'
When you use a regular expression for the route definition, capture groups are provided in the array using req.params[n]
, where n
is the nth capture group. This rule is applied to unnamed wild card matches with string routes such as /file/*
:
// GET /file/javascripts/jquery.js
console.dir(req.params[0])
// => 'javascripts/jquery.js'
If you need to make changes to a key in req.params
, use the app.param
handler. Changes are applicable only to parameters already defined in the route path.
Any changes made to the req.params
object in a middleware or route handler will be reset.
NOTE: Express automatically decodes the values in req.params
(using decodeURIComponent
).
Make sure to read the documentation on route parameters in the guide to routing. Lots of useful information there. In particular, the name of route parameters must be made up of "word characters" (i.e., [A-Za-z0-9_]
or \w
in regex). Additionally, to have more control over the exact string that can be matched by a route parameter, you can append a regular expression in parentheses at the end of the wildcard:
Route path: /user/:userId(\d+)
Request URL: http://localhost:3000/user/42
req.params: { "userId": "42" }
Returning to our own use case, we could have something like the following:
app.get('/story/:storyId', (req, res, next) => {
const { storyId } = req.params;
// simulate dynamic link generation (would likely be a pull from a database)
const link = Math.random();
res.send(`<h1>Story ${storyId}. <a href="/story/${storyId}/${link}">[Read more.]</a></h1>`)
})
So we send the user some HTML that mentions their requested story and its ID. Further suppose we wanted to have an option for the reader to read even more about their story. Then we could link them to another route that made further use of the parameters:
app.get('/story/:storyId/:link', (req, res, next) => {
const { storyId, link } = req.params;
res.send(`<h1>This is the link: ${link}. You are now reading more about story ${storyId}.</h1>`)
})
Note that req.params
contains all the wildcards in the route, namely storyId
and link
in the example just above, but it could be a lot more. The second route above will only trigger if we have /story/<something>/<something>
while the first route will only trigger at /story/<something>
.
To see this in action in the real world, consider going to an NFL story through ESPN:
https://www.espn.com/nfl/story/_/id/29039828/sources-member-chargers-tests-positive-covid-19
And go to another NFL story:
https://www.espn.com/nfl/story/_/id/29039856/green-bay-packers-hall-famer-willie-davis-dies-85
The /nfl/story
route is going to be incredibly common at ESPN. The catch here is what comes after the id
. The developers are going to pull out the story ID the same way we are. Basically for all of the NFL stories we will get the same route except a different id
and a different title
after the id
. This is very useful and very powerful. Query strings are ugly and sort of not cool, whereas keeping your URLs nice and clean is very friendly and these are easy to link to other people because they follow a natural convention.
It's worth noting that we have to be at least somewhat mindful when organizing our routes. For example, we wouldn't want to have something like the following:
app.get('/story/:storyId', (req, res, next) => {
const { storyId } = req.params;
// simulate dynamic link generation (would likely be a pull from a database)
const restOfStoryLink = Math.random();
res.send(`<h1>Story ${storyId}. <a href="/story/${storyId}/${restOfStoryLink}">[Read more.]</a></h1>`)
})
// The below route handler will never run as it is because the route above is matched first and a response sent
// We could throw a next() on at the end of the above route but that's very poor design
app.get('/story/:blogId', (req, res, next) => {
...
})
What you can do if you need something sort of similar to what's communicated above is use app.param
. The basic info is that app.param([name], callback)
takes two arguments:
- A
param
to look for in the route. - The callback to run with the usual suspects (i.e.,
req
,res
,next
) but also a fourth parameter that is the value of theparam
from the first argument.
The idea is that instead of something like
app.get('/story/:storyId', (req, res, next) => {
// ...
})
along with
app.get('/story/:blogId', (req, res, next) => {
// ...
})
where we try to handle the story based on whether or not we were dealing with a storyId
or a blogId
on the /story
route, we could instead bundle the functionality of both into something like
app.get('/story/:generalStoryId', (req, res, next) => {
// ...
})
but first use app.param
to effect how the /story/:generalStoryId
route will behave.
To make it clearer (a code snippet will follow this explanation): Suppose someone shows up on port 3000 with an HTTP request (this kicks everything into action on our server). Before we actually get to any route, app.param
will run (make sure you place anything using app.param
before your route handlers). We want to check if the route that is about to run (/story/:generalStoryId
in our case) has the specified parameter in it (in our case generalStoryId
). That means we want Express to go looking for the various app.METHOD
handlers and check to see if the one that is going to run has :<first-arg-to-app.param()>
anywhere inside of the route parameters: if it does, then the app.param
callback function will run.
If we have a qualifier, as we would if we had a request to the /story/<something>
route and had the following in our code
app.param('generalStoryId', (req, res, next, generalStoryId) => {
// ...
})
app.get('/story/:generalStoryId', (req, res, next) => {
// ...
})
then app.param
gives us a chance to modify the request/response object however we want before the route handler for /story/:generalStoryId
actually executes. As noted above in the skeleton of app.param
, the usual arguments for the middleware callback function are req
, res
, and next
, but with app.param
we also get a fourth argument, namely the value of the parameter used as the first argument to app.param
. So the generalStoryId
in (req, res, next, generalStoryId)
above is really equivalent to req.params.generalStoryId
in the actual request object. So it saves you a little bit of typing basically. And you can do anything you want inside of this callback function (just make sure you call next
at the end of it!). This is very nice as a kind of internal piece of middleware so that instead of having to do a bunch of string-checking or regular expressions to try to figure out whether or not the user is at a route that would qualify for
app.get('/story/:storyId', (req, res, next) => {
// ...
})
or
app.get('/story/:blogId', (req, res, next) => {
// ...
})
we can simply handle our conditional logic in app.param
and then have a single route handler to take care of things. Essentially, app.param()
functions as a sort of "route sanitizer" where the sanitizing has to do with what the wildcard is before the request gets to "the main route handler." The example below will make all of this clearer.
Example
Consider the following very contrived example:
app.param('generalStoryId', (req, res, next, generalStoryId) => {
console.log('Param called: ', generalStoryId);
/* The conditional logic:
- We can modify the request/response objects how we please before they get to our desired route
- We can then make use of our logic here in our desired route
- For example, you may want to set any number of local variables (on res.locals) or whatever
*/
switch(true){
case(generalStoryId > 0 && generalStoryId < 366):
res.locals.storyType = 'daily';
break
case(generalStoryId < 1000000):
res.locals.storyType = 'news';
break
case(generalStoryId < 1000000000):
res.locals.storyType = 'blog';
break
default:
res.locals.storyType = '';
break
}
next();
})
app.get('/story/:generalStoryId', (req, res, next) => {
const { generalStoryId } = req.params;
const { storyType } = res.locals;
// simulate dynamic link generation (would likely be a pull from a database)
res.locals.restOfStoryLink = Math.random(); // <-- maybe something from DB
res.locals.generalStoryId = generalStoryId;
res.locals.storyType === '' ? res.send(`<h1>Whoops! This story does not exist.</h1>`) : res.render(storyType);
})
app.get('/story/:generalStoryId/:link', (req, res, next) => {
console.log('The params: ', req.params)
const { generalStoryId, link } = req.params;
// the below would likely be res.render based on the link pulled from the DB somehow
res.send(`<h1>This is the link: ${link}. You are now reading more about story ${generalStoryId}.</h1>`)
})
The basic idea is that whenever a request is made to the route /story/<something>
, how we handle this request is informed by app.param
. Whatever the something
is will determine what view is rendered for the user or where the user may be sent once the request hits the route that sends back the final response. In some ways, app.param
can almost be thought of as "routing middleware" or a way to modify the response object before the request hits the route that ultimately sends the final response.
In the example detailed above, if the user visited /story/:generalStoryId
, then we used app.param
to determine what storyType
should be added to res.locals
before sending the request to the following route where the view would be rendered:
app.get('/story/:generalStoryId', (req, res, next) => {
// ...
})
The above contrived use case is to have a daily
story rendered if the story id is between 1 and 365, inclusive, etc. app.param
can be very useful if you're dedicated to representing differently classed things after a single path name. And one of the views could look like
<link rel="stylesheet" type="text/css" href="/stylesheets/styles.css">
<div class="login-page">
<div class="form">
<p>Welcome to your <%= storyType %> story! You are reading about story <%= generalStoryId %> </p>
<p>Click <a href='/story/<%=generalStoryId%>/<%=restOfStoryLink%>'>this link</a> to read more.</p>
</div>
</div>
and another view could look drastically different.
Passing data: sending files (via res.download
) and dealing with headers already sent
error
We are now going to look at how to send files and what to do if the headers have already been sent (at least a use case for how to handle that error). All of this will make more sense once you actually build something more sizable.
To simulate a scenario where you might want to send a file manually (typically you would want to generate some kinds of files on the fly such as bank statements and the like), we can create a userStatements
folder at the root level in our project directory and drop a sample bank statement from Wikipedia into it and title it BankStatementAndChecking.png
.
Maybe instead of our site being a blog site of some sort it is a vacation site, where maybe you buy stuff and they store stuff for you, but maybe you want to see your statement too. We can copy the contents of welcome.ejs
into a new welcomeBank.ejs
view in the views
folder and then change it to be the following:
<link rel="stylesheet" type="text/css" href="/stylesheets/styles.css">
<div class="login-page">
<div class="form">
<h1>Welcome back to the site, <%= username %>!</h1>
<a href="/story/1">Story 1</a>
<a href="/story/2">Story 2</a>
<a href="/story/3">Story 3</a>
<a href="/statement">Download your statement</a>
<p><a href="/logout">Log out</a></p>
</div>
</div>
You have no doubt seen something like the above where you could download a statement or something else. You would expect a PDF probably or something similar. In a production environment, these kinds of files would be generated on the fly--you wouldn't have a folder with a pre-built PDF or PNG of every user's activity (most users wouldn't even want it so it would be an unnecessary strain in all regards). We're not going to dynamically generate images or documents or anything like that right now so we're just going to use a dummy folder with a dummy picture in it for right now. Based on the above code, we now need a GET
route for /statement
.
It should be clear that such content (i.e., a bank statement) could never be put in the public
folder. It contains sensitive data! This data can only be sent back in a public manner. So what are our options? Well, we could try res.sendFile
like so:
res.sendFile(path.join(__dirname, 'userStatements/BankStatementAndChecking.png'))
So with path
we will go to the file system, with __dirname
we'll grab the directory our server script is in, grab the particular file we are interested in, and send that back across the wire (make sure you have the path
module included). What might be the problem with this? The problem is that with sendFile
the browser is going to interpret that as, "Oh, I'm supposed to load this file's contents up" (i.e., the image will be rendered in the browser). And maybe that's what you want, but if the user wants to download the image then you've just forced them to have to right click it and "save as" and so forth. Not good user experience!
If we want to get more insight about how to possibly make a better user experience, we can head over to Postman and send a GET
request to http://localhost:3000/statement
and then inspect the headers. We will see a Content-Type
header with a value of image/png
. We will see how to use this in just a second. The thing to know is that the response object actually has a download method: res.download(path [, filename] [, options] [, fn])
.
From the docs: Beginning note: The optional options
argument is supported by Express v4.16.0 onwards.
This (i.e., res.download
) transfers the file at path
as an "attachment". Typically, browsers will prompt the user for download. By default, the Content-Disposition
header "filename=" parameter is path
(this typically appears in the browser dialog). Override this default with the filename
parameter.
When an error occurs or transfer is complete, the method calls the optional callback function fn
. This method uses res.sendFile()
to transfer the file.
The optional options
argument passes through to the underlying res.sendFile()
call, and takes the exact same parameters.
res.download('/report-12345.pdf')
res.download('/report-12345.pdf', 'report.pdf')
res.download('/report-12345.pdf', 'report.pdf', function (err) {
if (err) {
// Handle error, but keep in mind the response may be partially-sent
// so check res.headersSent
} else {
// decrement a download credit, etc.
}
})
Basically, the simple use for res.download
includes passing two arguments:
- The filename
- Optionally, what you want the filename to download as (i.e., a different name than what it actually is)
We have the first part already in path.join(__dirname, 'userStatements/BankStatementAndChecking.png')
. For the second part, maybe we don't want it coming through as BankStatementAndChecking.png
. Maybe we want it as JimsStatement.png
or maybe we want to even personalize it using a cookie set earlier when the user logged in:
const { username } = req.cookies;
res.download(path.join(__dirname, 'userStatements/BankStatementAndChecking.png'), `${username}sStatement.png`)
If we put this in our file right now, to see the change reflected, we need to clear the cache and hard reload. In Chrome, open the console and then right-click the refresh wheel and choose "Empty Cache and Hard Reload". You will not get that option unless you have the console open. (We have to do this because we need to break the cache because Chrome has decided how to handle /statement
.)
If we again look at this happening in Postman, then again we see Content-Type
header with image/png
as the value, but what we get now that we did not get before is a new header Content-Disposition
:
Content-Disposition -> attachment; filename="FredsStatement.png"
This was not set last time and now it is--this time it is set to attachment and it has a filename set for FredsStatement.png
. If you are using Postman to simulate this process, then you will need to navigate to the Body tab and under x-www-form-urlencoded
enter key-values of username: Fred
and password: x
and issue a POST
request to http://localhost:3000/process_login
. Our server will then set the username
cookie and Postman will have access to this and you can see it in the Cookies tab below where Body, Cookies, Headers, and Test Results tabs exist.
The main thing res.download
is going to do for you is to set the Content-Disposition
header. Then it's going to call res.sendFile
to actually send the file. So basically we set the Content-Disposition
header and then we call res.download
with the file. The browser sees the Content-Disposition
header as an attachment
and then concludes, "Oh, I'm supposed to download this. I'm not supposed to render this." To recap, res.download
is setting the appropriate headers for us so we can architect as good a user experience as possible! Specifically, it is setting the Content-Disposition
header to attachment
with a filename
of whatever the second argument is that we passed to res.download
or the "actual" filename if we didn't pass the optional second argument.
This is great! We could accomplish roughly the same behavior by doing the following instead:
res.set('Content-Disposition', 'attachment');
res.sendFile
But why do it manually when res.download
comes built-in and does it for us (because Express knows this is a commonly desired feature)? Express even has another response method for this kind of behavior: res.attachment([filename])
. As the docs note: This (i.e., res.attachment
) sets the HTTP response Content-Disposition
header field to "attachment". If a filename
is given, then it sets the Content-Type
based on the extension name via res.type()
, and sets the Content-Disposition
"filename=" parameter:
res.attachment()
// Content-Disposition: attachment
res.attachment('path/to/logo.png')
// Content-Disposition: attachment; filename="logo.png"
// Content-Type: image/png
The biggest difference here is that res.sendFile
is not called when using res.attachment
(i.e., calling this results in only setting headers but not immediately sending a file).
All this to say: res.download
is still probably the most intuitive and useful to use. Finally, one last thing to note about res.download
: There is a third optional argument that can be used beyond just the filepath to the file desired to be downloaded (first arg) or what we want the filename to be called upon download (the second, optional arg). This third (optional) argument is a callback function that comes with an error
object that is executed once everything is done:
res.download(path.join(__dirname, 'userStatements/BankStatementAndChecking.png'), `${username}sStatement.png`, (error) => {
console.log(error)
})
So above, if an error occurs, we'll know. Note again that the callback won't be run until the file transfer is complete. Something else to note here (and this really is the potentially sticky issue): If there is an error in sending the file, the headers may already be sent. That means you have already done your res
. You don't get another res.json()
or res.send()
or something like that. We cannot do something like what you may be thinking:
res.download(path.join(__dirname, 'userStatements/BankStatementAndChecking.png'), `${username}sStatement.png`, (error) => {
res.redirect('/download/error')
})
The intention above is clear: If there is an error in the attempt to download a file, then we want to redirect the user to a download error page. But we only get one res
and if the headers have already been sent, then we simply cannot make this redirect. You'll get this error whenever you try to send the client more than one response (e.g., maybe you have res.send('<h1>Hi!</h1>')
after a res.json({message: 'hello'})
). So we're going to have to figure out another way to handle this. Now, a way to figure out if the headers have already been sent is to check a boolean that is made available to us by Express: res.headersSent
. As the docs note: This is a boolean property that indicates if the app sent HTTP headers for the response:
app.get('/', function (req, res) {
console.dir(res.headersSent) // false
res.send('OK')
console.dir(res.headersSent) // true
})
So we might do something like the following:
res.download(path.join(__dirname, 'userStatements/BankStatementAndChecking.png'), `${username}sStatement.png`, (error) => {
// If there is an error in sending the file, then HTTP headers may already be sent
if(error) {
// If headers have *not* been sent, then redirect to an error page.
if(!res.headersSent) {
res.redirect('/download/error')
}
}
})
So basically we will redirect the user if headers have not already been sent but will have to figure something else out if they have. So we'll only try to redirect them if the headers have not been sent.
That covers the basics of how to download a file in about a single line. So remember: what res.download
really does is set the single HTTP header Content-Disposition
to attachment
which the browser knows what to do with. The browser makes a decision based on that.
And that is really what you have to remember as your job as a developer: all you have to work with is one response. You send something back to the browser and Firefox, Chrome, Safari, etc., have all agreed what to do if the Content-Disposition
header is set to attachment
. That is what a protocol is. You're following the rules. They (i.e., the browsers) are following the rules. We can't force the browser to do anything. We can only set the headers and then let the browser take over.
Miscellany: Advanced Routing, Express Generator, HTTP Headers, etc.
Advanced routing: making applications more modular by routing using express.Router([options])
We are now going to take a look at express.Router([options])
, the other main method we have not yet covered for the methods on the express
object. The router
object is almost like a microservices type architecture inside of your Express app--it essentially creates its own little mini application. Its only job is to handle middleware and to handle routes. That's all it does. It behaves like middleware but it's a really nice way to modularize your application. So the app.get
, app.post
, etc., that we have been using so far, the router
works exactly the same way, but the router
works in its own little container. You put it in its own folder to keep things straight. That is a really nice and important thing to do as a developer. Because as a developer when you start working through your application, you start trying to figure out where things are located, and you want to be able to know where the stuff is! That's why we create different directories and subdirectories and so forth.
So right now consider what we had before as our little login application:
// Require native node modules
const path = require('path');
// Require third-party modules
const express = require('express');
const app = express(); // invoke an instance of an Express application
const helmet = require('helmet');
const cookieParser = require('cookie-parser');
// Middleware used at the application level (i.e., on all routes/requests)
app.use(helmet());
app.use(express.static('public'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.use(cookieParser());
// Set default view engine and where views should be looked for by Express
app.set('view engine', 'ejs');
app.set('views', [
path.join(__dirname + '/views')
]);
// Custom middleware run at the application level
app.use((req, res, next) => {
if(req.query.msg === 'fail') {
res.locals.msg = 'Sorry. This username and password combination does not exist.';
} else {
res.locals.msg = '';
}
next();
})
/////////////// JUST ROUTES BELOW (and where server is listening) ///////////////
app.get('/', (req, res, next) => {
res.json({
message: 'Sanity check good'
});
})
app.get('/login', (req, res, next) => {
res.render('login');
})
app.post('/process_login', (req, res, next) => {
const { username, password } = req.body;
if (password === 'x') {
res.cookie('username', username);
res.redirect(303, '/welcome');
} else {
res.redirect(303, '/login?msg=fail&test=hello');
}
})
app.get('/welcome', (req, res, next) => {
res.render('welcome', req.cookies);
})
app.param('generalStoryId', (req, res, next, generalStoryId) => {
switch(true){
case(generalStoryId > 0 && generalStoryId < 366):
res.locals.storyType = 'daily';
break
case(generalStoryId < 1000000):
res.locals.storyType = 'news';
break
case(generalStoryId < 1000000000):
res.locals.storyType = 'blog';
break
default:
res.locals.storyType = '';
break
}
next();
})
app.get('/story/:generalStoryId', (req, res, next) => {
const { generalStoryId } = req.params;
const { storyType } = res.locals;
// simulate dynamic link generation (would likely be a pull from a database)
res.locals.restOfStoryLink = Math.random(); // <-- maybe something from DB
res.locals.generalStoryId = generalStoryId;
res.locals.storyType === '' ? res.send(`<h1>Whoops! This story does not exist.</h1>`) : res.render(storyType);
})
app.get('/story/:generalStoryId/:link', (req, res, next) => {
const {generalStoryId, link} = req.params;
res.send(`<h1>This is the link: ${link}. You are now reading more about story ${generalStoryId}.</h1>`)
})
app.get('/statement', (req, res, next) => {
const {username} = req.cookies;
res.download(path.join(__dirname, 'userStatements/BankStatementAndChecking.png'), `${username}sStatement.png`)
})
app.get('/logout', (req, res, next) => {
res.clearCookie('username')
res.redirect(303, '/login');
})
app.listen(3000);
All in all, this application is not that big yet. But as your application grows so will your code base. And if we look at the code above, then at the top we will see all of the middleware used at the application level (i.e., the middleware parsing the body, serving up static files, setting the views, etc.). Right now the routes really aren't so bad, but if you start adding stuff like database access, business logic, etc., then you will suddenly have a monolithic app. The point is that the router
makes it very easy to modularize things so it would be silly not to take advantage of the functionality it offers right out of the box.
We are now going to create two new files, routerApp.js
and theRouter.js
. The way that our application is going to work is that routerApp.js
will house our actual application (i.e., all of the middleware we use at the application level, what server we are listening on, etc.). And theRouter.js
is going to be where the router lives (i.e., where we are going to handle all of our routes). This will make a lot more sense momentarily.
The routerApp.js
is easy to create and just like what we have been doing all along (except this time we will not include a view engine--we will respond with JSON as if we are building an API).
//routerApp.js
// Third-party modules
const express = require('express');
const app = express();
const helmet = require('helmet');
// Middleware used at the application level
app.use(helmet());
app.use(express.json())
app.use(express.urlencoded());
app.use(express.static('public'));
//////////////////////////////////////////////////
// NORMALLY WHERE THE ROUTES WOULD GO
// WE WANT TO MODULARIZE THIS NOW
// THIS LOGIC WILL GO IN theRouter.js
//////////////////////////////////////////////////
app.listen(3000);
That's it. Now let's talk about the construction process of theRouter.js
. Since this will be a different file from routerApp.js
and we want to use the Router
method on the express
object, we actually need const express = require('express')
at the top of this file. Directly underneath we will create an instance of the router
object:
const express = require('express');
let router = express.Router();
As always, check the docs for more about express.Router([options])
. It's fairly simple: you create a new router object as we did above: let router = express.Router();
. Typically, you will probably not want to use the optional options
parameter (all of the options make things more strict), but it's good to know about them because they specify how the router
is to behave (note that mergeParams
is only available with version 4.5.0+
of Express):
Property | Description | Default |
---|---|---|
caseSensitive | Enable case sensitivity. | Disabled by default, treating "/Foo" and "/foo" as the same. |
mergeParams | Preserve the req.params values from the parent router. If the parent and the child have conflicting param names, the child's value take precedence. | false |
strict | Enable strict routing. | Disabled by default, "/foo" and "/foo/" are treated the same by the router. |
As the docs note under this table, you can add middleware and HTTP method routes (such as get
, put
, post
, and so on) to router
just like an application.
The router
works the same way that app
has in how we have previously handled routing. It's just that now routing is specific to this router. The docs have some beginning notes on the Router object that are worth reproducing below (the docs then simply explore all the methods on the router
object which we will be exploring ourselves).
From the docs: A router
object is an isolated instance of middleware and routes. You can think of it as a "mini-application," capable only of performing middleware and routing functions. Every Express application has a built-in app router.
A router behaves like middleware itself, so you can use it as an argument to app.use()
or as the argument to another router's use() method.
The top-level express
object has a Router() method that creates a new router
object.
Once you've created a router object, you can add middleware and HTTP method routes (such as get
, put
, post
, and so on) to it just like an application. For example:
// invoked for any requests passed to this router
router.use(function (req, res, next) {
// ... some logic here ... like any other middleware
next()
})
// will handle any request that ends in /events
// depends on where the router is "use()'d"
router.get('/events', function (req, res, next) {
// ...
})
You can then use
a router for a particular root URL (thus effectively separating your routes into files or even mini-apps):
// only requests to /calendar/* will be sent to our "router"
app.use('/calendar', router)
So instead of using app.get(...)
as we have done previously, we will now do router.get(...)
. These two things will do exactly the same thing. The difference is that the router
in router.get(...)
is made specifically for this purpose, whereas app
can do anything and it's done at the application level (hence, app(lication).get
). The router
gives us a little bit more of an option in terms of modularizing the application and creating a nicer long-term architecture. So this is how we will do things from now on.
So right now we will start with a simple
router.get('/', (req, res, next) => {
res.json({
msg: 'Router works!'
})
})
where we still have access to the req
, res
, and next
objects just as we did before when using app
. Why? Recall from the docs: "A router behaves like middleware itself, so you can use it as an argument to app.use()
or as the argument to another router's use()
method." We have not brought this router into routerApp.js
yet, but the point is we can put something like app.use('/', router)
, and this will inform Express that we want to use this router whenever a user makes a request to the root page of our application (note that this will invoke the usage of the router for any type of request to /
because we are utilizing app.use
instead of app.get
or something like that).
So how can we actually use the router in our application? Since theRouter.js
is a file, in order for our application to use or consume it, we are going to need to export it. So we need to drop a little module.exports = router
which comes straight from Node.js (not an Express-centric thing). This will send back the router to whomever is asking for it via module.exports
.
So right now theRouter.js
would look something like this:
const express = require('express');
let router = express.Router();
router.get('/', (req, res, next) => {
res.json({
msg: 'Router works!'
})
})
module.exports = router;
And now we can use the router in our application like so:
// Third-party modules
const express = require('express');
const app = express();
const helmet = require('helmet');
// Middleware used at the application level
app.use(helmet());
app.use(express.json())
app.use(express.urlencoded({extended: false}));
app.use(express.static('public'));
//////////////////////////////////////////////////
// NORMALLY WHERE THE ROUTES WOULD GO
// WE WANT TO MODULARIZE THIS NOW
// THIS LOGIC WILL GO IN theRouter.js
//////////////////////////////////////////////////
// Routes we want to use at different levels of the application
const router = require('./theRouter');
// Enlisting the routes brought in to actually use in the application
app.use('/', router);
app.listen(3000);
So to make use of the router:
- Import it into the main application:
const router = require('./theRouter')
- Use it in the application where you see fit:
app.use('/', router)
In truth, we would probably want to import the router for /
not as router
but as something more descriptive like indexRouter
. Similarly, if we want a whole series of middleware and routing to apply whenever a user hits the /story
route, then we would probably want to have a storyRouting.js
file (similar to our theRouter.js
file) that we require
ed from our application and used like so:
const storyRouter = require('storyRouting');
app.use('/story', storyRouter);
NOTE: module.exports and importing files |
---|
If we export only the router from a file with routing logic like module.exports = router , then by default the router being exported is considered a default export and we can simply import it, name it whatever we want, and use it by referring to what we have named it. For example, previously we had const router = require('theRouter') . We do not, and almost always will not, refer to the router being exported simply as router . Usually we will name it so it reflects what kind of routing logic it is supposed to contain. Since the router object is often the only thing being exported from a routing file (and hence considered the default export ), we can name the incoming router object whatever we please. That is: const whatever-name-you-want = require('routerFile') will work. However, if, for some reason, more than one thing is being exported from the routing file, then you will need to be more cautious.For example, suppose, for some crazy reason, that our routing file for /story had the following at the end of it: module.exports = { router, bestSport: 'Basketball' }; . Then our application file would need to bring the router object in like so: const { router: storyRouter } = require('storyRouting') . We are simply using ES6 to destructure and rename since router is no longer the default export from storyRouter.js . See this article for more about destructuring and renaming. This is not an Express thing, but it's helpful to know. |
So real quick let's review what's happening in our basic theRouter.js
file:
const express = require('express');
let router = express.Router();
router.get('/', (req, res, next) => {
res.json({
msg: 'Router works!'
})
})
module.exports = router;
- We want to use Express (the
Router
method in particular) so we import theexpress
package and name its default exportexpress
. - We call the
Router
method on theexpress
object and assign the return value torouter
. - We create a whole bunch of middleware and routes in our routing file (right now just one route). The middleware can be standalone (i.e., named or anonymous functions that manipulate
req
andres
in some way) or used in the routing (i.e., as an anonymous callback function as is being done above). The point, however, is that all the methods we might normally use at the application level withapp.METHOD
are now being used in a more modular, containerized fashion withrouter.METHOD
, where therouter
is only going to be applied in the application where we designate withapp.use('/where-to-use-router')
, and thisrouter
will be used for all requests to/where-to-use-router
since we utilizedapp.use
instead ofapp.get
or something else more restrictive. - We export the
router
. But notice what all has been added to therouter
. Did we make any modifications to therouter
object? Of course we did! That is the whole point. We dumped a bunch of logic into the different methods forrouter
to actually make use of in our application:router.get(...logic...)
,router.post(...logic...)
, etc. If it helps, think of this like adding properties or methods to "normal objects" in JavaScript. For example, we can declare a normal object like so:let myDog = { name: 'Bowser' }
. Now let's define a method formyDog
:myDog.getName = function() {return this.name};
. Finally, let's actually call this method:myDog.getName() // Bowser
. Think of this whole process withrouter
like what we just did withmyDog
. You attach a bunch of logic to the built-inget
,post
, etc., methods onrouter
and then this logic fires off once those kinds of requests are made. When we export therouter
object, we export all of the logic we defined for its different methods too.
What the exported router
object looks like based on methods and middleware you use (depp dive)
To make the note immediately above more concrete (i.e., that whatever route handling we do with route.METHOD
or whatever middleware we define and use within these routes is basically like tacking on methods for ordinary JavaScript objects), we will take a look under the hood and see what router
looks like after using router.METHOD
and associated middleware. First take a look at the beginning of the the docs about the router
object: A router
object is an isolated instance of middleware and routes. You can think of it as a "mini-application," capable only of performing middleware and routing functions. Every Express application has a built-in app router.
That said, consider the following two files we will use to illustrate what is going on under the hood:
// routerApp.js // this is the main application
// Third-party modules
const express = require('express');
const app = express();
const helmet = require('helmet');
// Middleware used at the application level
app.use(helmet());
app.use(express.json())
app.use(express.urlencoded({extended: false}));
app.use(express.static('public'));
//////////////////////////////////////////////////
// NORMALLY WHERE THE ROUTES WOULD GO
// WE WANT TO MODULARIZE THIS NOW
// THIS LOGIC WILL GO IN theRouter.js
//////////////////////////////////////////////////
// Routes we want to use at different levels of the application
const router = require('./theRouter');
console.log('This is the imported router: ', router);
// Enlisting the routes brought in to actually use in the application
app.use('/', router)
app.listen(3000);
And then one of the routes we will export as router
:
// theRouter.js // our router and the router to be imported into the main application
const express = require('express');
let router = express.Router();
function validateUser(req, res, next) {
res.locals.validated = true;
next();
}
// router.use(validateUser);
router.get('/', (req, res, next) => {
res.json({
msg: 'Router works!'
})
})
router.post('/', (req, res, next) => {
res.json({
msg: 'Router works!'
})
})
module.exports = router
We'll get to why router.use(validateUser)
is commented out momentarily, but let's first see what router
looks like when we log it to the console after firing up the app:
This is the imported router: [Function: router] {
params: {},
_params: [],
caseSensitive: undefined,
mergeParams: undefined,
strict: undefined,
stack: [
Layer {
handle: [Function: bound dispatch],
name: 'bound dispatch',
params: undefined,
path: undefined,
keys: [],
regexp: /^\/?$/i,
route: [Route]
},
Layer {
handle: [Function: bound dispatch],
name: 'bound dispatch',
params: undefined,
path: undefined,
keys: [],
regexp: /^\/?$/i,
route: [Route]
}
]
}
It looks like there's a stack
property on the router
object whose value is an array of middleware functions or Layer
s used within the router just imported. Specifically, each Layer
appears to refer to the middleware we are using for the different METHODS
on the router
object. That is, the first Layer
appears to point to router.get(...)
and the second layer appears to point to router.post(...)
. What about the validateUser
middleware in the router
? Well, unless we use it in the router
, then it will not get pushed onto the stack
. So if we now uncomment router.use(validateUser);
, then we will see something like the following:
This is the imported router: [Function: router] {
params: {},
_params: [],
caseSensitive: undefined,
mergeParams: undefined,
strict: undefined,
stack: [
Layer {
handle: [Function: validateUser],
name: 'validateUser',
params: undefined,
path: undefined,
keys: [],
regexp: /^\/?(?=\/|$)/i,
route: undefined
},
Layer {
handle: [Function: bound dispatch],
name: 'bound dispatch',
params: undefined,
path: undefined,
keys: [],
regexp: /^\/?$/i,
route: [Route]
},
Layer {
handle: [Function: bound dispatch],
name: 'bound dispatch',
params: undefined,
path: undefined,
keys: [],
regexp: /^\/?$/i,
route: [Route]
}
]
}
Because the middleware was actually used, this got pushed onto the stack
of our router
object. If you're really in the mood, you can investigate this behavior further by digging into the express node module:
node_modules -> express -> lib -> router -> index.js
If you look around in index.js
you will come across proto.use = function use(fn) { ... }
and inside of the function body you will see this.stack.push(layer);
. If you are feeling adventurous, then you can even put console.log('MIDDLEWARE FUNCTIONS BEING USED: ', this)
right before the final line of the function (i.e., return this
) to see what this
refers to. When app.use(validateUser);
is not commented, then the validateUser
function will show up in the stack
(along with a bunch of other middleware being used that we don't explicitly interact with like query
, expressInit
, helmet
, jsonParser
, urlencodedParser
, serveStatic
, etc.)
And really this should make sense given the structure of our very basic application thus far:
// Third-party modules
const express = require('express');
const app = express();
const helmet = require('helmet');
// Middleware used at the application level
app.use(helmet());
app.use(express.json())
app.use(express.urlencoded({extended: false}));
app.use(express.static('public'));
const router = require('./theRouter');
app.use('/', router)
app.listen(3000);
Given the above, is it any surprise to find expressInit
, helmet
, jsonParser
, etc.? Simply recall what res.json
does. It parses JSON for us and it is based on the body-parser
node module. If we do a quick search we can even see the jsonParser
function being exported:
node_modules -> body-parser -> lib -> types -> json.js
You will see in this file module.exports = json
. Well what is json
? It's a function that returns jsonParser
, where jsonParser
is middleware because it has access to req
, res
, and next
, made evident in the signature of the function.
The point of all of this is simply to emphasize that within whatever routing file you create, the router
object you define and subsequently apply middleware to (whether it's middleware anonymous callback functions within METHODS
on router
such as get
, post
, etc.) is going to be part of the router
object you export. Basically, the routing file you create is just a way of building the router
object for a certain route which you then export to the main application and use at the application level on the designated/desired route.
By including app.use('/', router);
in our main application, let's consider what we are effectively communicating to Express. The '/', router
in app.use('/', router);
is regular old middleware. It means, "Hey, app
! At the /
path, I want you to use this thing: router
." That means that everything in the file from which router
came is going to be used at the /
route.
This next point is incredibly important and basically how all of the routing is structured/configured: All of the paths in the routing file are relative to the path specified in app.use
. That is, if we have a file storyRouting.js
that exports a router we call storyRouter
in the main application, where subsequently we declare app.use('/story', storyRouter);
in the main application, then this means that all of the routes in storyRouting.js
are relative to /story
since that was the path specified in app.use('/story', storyRouter);
.
Essentially, this is like prepending /story
to every route in storyRouting.js
. The storyRouting.js
file is only meant to handle requests (of any kind) to /story
. To put it more clearly, suppose we had the following routes in our main application (much as we used to do by including all of the routing in our main application regardless of the path):
app.get('/story/topic', ...);
app.post('/story/topic', ...);
app.get('/story/author/:authorId', ...);
app.post('/story/title/:authorId');
// ...
app.get('/television/show/:showId', ...);
app.get('/television/review/:reviewId', ...);
app.delete('/television/show/:showId', ...);
app.put('/television/review/edit/:reviewerId', ...);
// ...
Notice a pattern? We're handling a lot of traffic to /story
as well as to /television
. You can imagine a bunch of other traffic for different routes depending on the application. We can clean up the code above by creating routers to handle the /story
and /television
routes separately. In our main application, we would first probably build a routes
folder to house all of our routes and then include something like the following:
// import routers
const storyRouter = require('./routes/storyRouting'); // import router from storyRouting.js
const televisionRouter = require('./routes/televisionRouting'); // import router from televisionRouting.js
// use routers in main application
app.use('/story', storyRouter);
app.use('/television', televisionRouter);
Then our storyRouting.js
file would look something like this:
// storyRouting.js
const express = require('express');
let router = express.Router();
router.get('/topic', ...);
router.post('/topic', ...);
router.get('/author/:id', ...);
router.post('/title/:authorId', ...);
// ...
module.exports = router // <-- imported as storyRouter in main application
And our televisionRouting.js
file would look something like this:
// televisionRouting.js
const express = require('express');
let router = express.Router();
router.get('/show/:showId', ...);
router.get('/review/:reviewId', ...);
router.delete('/show/:showId', ...);
router.put('/review/edit/:reviewerId', ...);
// ...
module.exports = router // <-- imported as televisionRouter in main application
The idea is that we can have lots of routers that only trigger based on requests to certain paths. All of this may seem somewhat abstract at first, but it's nothing we haven't seen before. app.use
in general simply means, "Hey, we're about to add some middleware to our application." In the case of something like app.use('/story', storyRouter);
, it still means the same thing: "Hey, I want to add a bunch of middleware to my application for any kind of request to the /story
route and I want the middleware defined in storyRouter
to apply to such requests."
One thing to note, which you saw if you looked at the note about what the exported router
actually likes like from a file that is exporting a router to use in the application, is what router.use
is all about. Just like app.use
means, "Hey, I want to apply some middleware to this application for any kind of request.," we have router.use
which similarly means, "Hey, I want to apply some middleware to THIS ROUTER for any kind of request (i.e., the middleware will be applied to any request that hits the path for which our router is handling traffic)." So again this makes sense in light of thinking of routers as almost defining their own little mini applications.
This is very nice because it keeps our middleware as well as our routes separate from the main application. The router is almost a way to create a bunch of tiny little applications inside of your main app sort of like a microservices kind of architecture. The router will allow you to keep your main application very clean. You will have in the main application things that need to be done everywhere.
Express generator: building an Express application scaffold from the command line
One thing to get out of the way right up front: The express generator does not include helmet
by default. So make sure sure you add it as soon as you employ the generator! That said ...
The express generator is awesome. A lot of the starter code we write in our files can be abstracted. The express generator is a command line utility that builds out a decent amount of boilerplate code and project structure that we have been doing ourselves repeatedly.
As the docs note, you can either run npm install -g express-generator
followed by express
or just npx express-generator
to build out the base level of an express app with the default options included (i.e., view engine, css processor if any, etc.). Read this article about npx
vs npm
. Essentially, npx
ensures you are always getting the latest, stable version of Express (as opposed to downloading it globally and then it possibly changing a ways down the road later) so we will use npx
for this reason.
Generally speaking, the way you will use the generator is as follows:
npx express-generator --view=ejs --css=sass appName
Some things to note about the above:
npx
: You are usingnpx
to get the project started.express-generator
: This is the command line utility you wantnpx
to execute (kind of likenpx create-react-app
or whatever other kind of command line utility you might use).--view=
: View engines supported:ejs|hbs|hjs|jade|pug|twig|vash
. Use--no-view
instead if you plan not to use a view engine. Defaults to Pug when nothing is specified (i.e.,--view=pug
).--css
: Processors supported:less|stylus|compass|sass
. Defaults to plain css when nothing is specified.appName
: Whatever you want your application/project directory to be called (inside this directory is where all of the boilerplate code will go).
So, for example, we might do something like
npx express-generator --view=ejs --css=sass generatedApp
to create an Express app in a folder named generatedApp
that uses EJS as its default view engine and Sass as its default CSS processor. This is the directory structure we would get as a result of this:
generatedApp
┣ bin
┃ ┗ www
┣ public
┃ ┣ images
┃ ┣ javascripts
┃ ┗ stylesheets
┃ ┃ ┗ style.sass
┣ routes
┃ ┣ index.js
┃ ┗ users.js
┣ views
┃ ┣ error.ejs
┃ ┗ index.ejs
┣ app.js
┗ package.json
Notice the lack of any node_modules
. As the docs note, as soon as we create the folder that houses our project, we need to cd
into that folder and run npm install
to download all of the project dependencies (we do not need to run npm init
because we already have a package.json
that lists all of the dependencies we will need to install--these dependencies are installed via npm install
).
Take a look back at the directory structure! It's very similar to everything we have done up to this point. We have public
, routes
, and views
folders. Everything is structured as we might hope/expect except for one minor funky thing with a bin
folder that actually represents the entry point for our application.
A (lengthy) note about bin/www
This question and this question on Stack Overflow can shed some light. Essentially, the idea is that app.js
(not in bin
) contains all the middleware and routes you need to get started and at the bottom it exports the app
object. Thus far, we always had a sort of "main application" file where we used app.listen(3000);
, but what do we have now? Furthermore, where is our app
actually require
d if we are exporting it?
It makes sense that our app
would be require
d in www
in bin
since www
is the entry point for our application, but what is going on in www
that actually makes it the entry point of our application? What does www
accomplish? To answer this, we really need to take a walk down memory lane (when we first built a server in Node.js only without any Express). In particular, we need to look at the docs for app.listen
or app.listen([port[, host[, backlog]]][, callback])
(reproduced below to add as much useful context as possible, along with personal notes):
Walk down memory lane (personal notes informed by documentaton)
app.listen([port[, host[, backlog]]][, callback])
binds and listens for connections on the specified host and port. This method is identical to Node's http.Server.listen()
. From the docs for server.listen()
: Starts the HTTP server listening for connections. Well, how do we create an HTTP server on which to listen for connections? We use http.createServer
or, more completely, http.createServer([options][, requestListener])
. Useful default options
are taken care of for us, but we almost always want to pass in a requestListener
which is a Function
(why wouldn't you want to listen to requests to your server!?). What does http.createServer
return? An HTTP server hopefully! Yes, it returns a new instance of http.Server
, where the requestListener
is a function which is automatically added to the request
event. Well, note that Class: http.Server
has server.listen()
as a method for whatever HTTP server
we are considering.
Why is any of this important? Well, to truly walk down memory lane, recall our basic Node.js server where we didn't use any Express at all and tried to handle the most basic of routing situations:
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, { 'content-type': 'text/html' });
const homePageHTML = fs.readFileSync('node.html');
res.write(homePageHTML);
res.end();
} else if (req.url === "/node.png") {
res.writeHead(200, { 'content-type': 'image/png' });
const image = fs.readFileSync('node.png');
res.write(image);
res.end();
} else if (req.url === "/styles.css") {
res.writeHead(200, { 'content-type': 'text/css' });
const css = fs.readFileSync('styles.css');
res.write(css);
res.end();
} else {
res.writeHead(404, { 'content-type': 'text/html' });
res.write(`<h4>Sorry, this isn't the page you're looking for!</h4>`)
res.end()
}
});
server.listen(3000);
Notice that we set server
to http.createServer(...)
where the ...
was just a big requestListener
function that served as the basis for our painfully tedious little app/server. This requestListener
function literally is our application in this case. That is, we listen for requests and we handle them with our requestListener
function.
And this is really what is going on behind the scenes in Express (remember Express is Node). Without using anything from Express, the key to our plain Node.js application was the following:
const http = require('http'); // <-- make use of native http module
const server = http.createServer((req, res) => { // <-- create an instance of an http server and pass it an anonymous requestListener function
// ... <-- a bunch of ways defining how to respond to requests to different routes
});
server.listen(3000); // <-- call the listen method on the http server instance and pass it a port number
We could accomplish the same thing in Express using the express
and http
modules:
const http = require('http');
const express = require('express');
const app = express();
// app.METHODS <-- define requestListeners for METHOD requests to specified routes
http.createServer(app).listen(3000); // <-- call the listen method on http server instance with port number
The specific thing to note here is that app
is the requestListener
function we pass to http.createServer
. But instead of defining app
in place as an anonymous function as the requestListener
argument to http.createServer
, as we did with the plain Node.js server, we actually modify the base app
function considerably by using things like app.get
, app.post
, app.use
, app.SOMETHING
before passing it to http.createServer
. In fact, if we're really in the mood, then we can "refactor" the plain Node.js application/server as follows:
const http = require('http');
const fs = require('fs');
const app = function (req, res) {
if (req.url === '/') {
res.writeHead(200, { 'content-type': 'text/html' });
const homePageHTML = fs.readFileSync('node.html');
res.write(homePageHTML);
res.end();
} else if (req.url === "/node.png") {
res.writeHead(200, { 'content-type': 'image/png' });
const image = fs.readFileSync('node.png');
res.write(image);
res.end();
} else if (req.url === "/styles.css") {
res.writeHead(200, { 'content-type': 'text/css' });
const css = fs.readFileSync('styles.css');
res.write(css);
res.end();
} else {
res.writeHead(404, { 'content-type': 'text/html' });
res.write(`<h4>Sorry, this isn't the page you're looking for!</h4>`)
res.end()
}
}
const server = http.createServer(app);
server.listen(3000);
This should make sense! Recall that app
returned by express()
is in fact a JavaScript Function
, namely createApplication
. Remember inspecting the express node module when we first started looking at Express?
node_modules -> express -> lib -> express.js
And in express.js
we saw exports = module.exports = createApplication
and right below this line:
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
// expose the prototype that will get set on requests
app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
// expose the prototype that will get set on responses
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
app.init();
return app;
}
Hence, invoking express()
causes a default export of the createApplication
function which itself returns an initialized app
capable of handling the request and response objects for HTTP traffic. Notice that the app
returned is what we have been dealing with from the start in "Express land": a middleware function.
So if
const http = require('http');
const express = require('express');
const app = express();
// app.METHODS <-- define requestListeners for METHOD requests to specified routes
http.createServer(app).listen(3000); // <-- call the listen method on http server instance with port number
works just fine, then what is app.listen
in Express really doing anyway? Inspect the node modules (the docs tell us too):
node_modules -> express -> lib -> application.js
Towards the end of the file you will see something like the following:
/**
* Listen for connections.
*
* A node `http.Server` is returned, with this
* application (which is a `Function`) as its
* callback. If you wish to create both an HTTP
* and HTTPS server you may do so with the "http"
* and "https" modules as shown here:
*
* var http = require('http')
* , https = require('https')
* , express = require('express')
* , app = express();
*
* http.createServer(app).listen(80);
* https.createServer({ ... }, app).listen(443);
*
* @return {http.Server}
* @public
*/
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
That's it! As the docs note and we explicitly see above in the source code: the app.listen()
method returns an http.Server
and (for HTTP) is a convenience method for the relevant part of what we see above:
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
This is literally the same thing we did with the following code:
const http = require('http');
const express = require('express');
const app = express();
// app.METHODS <-- define requestListeners for METHOD requests to specified routes
http.createServer(app).listen(3000); // <-- call the listen method on http server instance with port number
So really app.listen()
is exactly what it claims itself to be: a convenience method. We don't have to write
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
every time. We can simply write app.listen(port-number)
. As the docs further note, the app
returned by express()
is designed to be passed to Node's HTTP servers as a callback to handle requests. This makes it easy to provide both HTTP and HTTPS versions of your app with the same code base, as the app does not inherit from these (it is simply a callback):
var express = require('express')
var https = require('https')
var http = require('http')
var app = express()
http.createServer(app).listen(80)
https.createServer(options, app).listen(443)
End of walk down memory lane
What does all of the above have to do with bin/www
? Well, if you look at the www
file, then you will see that nearly all of the code is really error handling. The code relevant to us, and everything noted above, is the following super trimmed down version (only relevant code reproduced below):
var app = require('../app'); // <-- import our beefy application/requestListener function
var http = require('http'); // <-- import native http module so we can create an HTTP server
var port = normalizePort(process.env.PORT || '3000'); // <-- get port from environment (or 3000 by default)
app.set('port', port); // <-- set port as the port number declared and initialized above
var server = http.createServer(app); // <-- create HTTP server and pass it app as requestListener function
server.listen(port); // <-- listen on provided port
So to summarize: www
creates an httpServer
and passes your app
as the requestListener
function, where app
was initially created by express()
and thereafter heavily modified by you in order for your application to behave as desired based on HTTP requests since app
is, after all, just a beefed up requestListener
function.
Besides this, www
also sets the port via server.listen(port)
where port
is either an environment variable accessed and then used or is set to 3000 by default (unless overridden by the user manually directly in www
or via an environment variable as just mentioned). Additonally, www
sets the functions to be called if there is an error while starting the server: server.on('error', onError)
.
In sum, www
basically removes all the create and start server code from your app.js
and lets you focus only on the application logic part.
A note about package.json
based on options you pass to express generator; and error-handling in Express
It should make sense that our package.json
will look different based on what kind of options we pass to the express generator (since package.json
lists all of our project dependencies). Of course, the package.json
may look different in all sorts of cases but the biggest ones will be the following:
1. Include a view engine (e.g., ejs|hbs|hjs|jade|pug|twig|vash
) and a CSS engine (e.g., less|stylus|compass|sass
)
Example generator code:
npx express-generator --view=ejs --css=sass generatedApp
Resultant
package.json
:// view engine (ejs) and CSS engine (sass)
{
"name": "generatedapp",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"ejs": "~2.6.1",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
"node-sass-middleware": "0.11.0"
}
}
2. Include a view engine but no CSS engine
Example generator code:
npx express-generator --view=hbs appWithViewPlainCSS
Resultant
package.json
:// view engine (hbs) but no CSS engine (plain css)
{
"name": "appwithviewplaincss",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"hbs": "~4.0.4",
"http-errors": "~1.6.3",
"morgan": "~1.9.1"
}
}
3. Do not include a view engine but do include a CSS engine
Example generator code:
npx express-generator --no-view --css=less appWithoutViewLessCSS
Resultant
package.json
:// no view engine but uses a CSS engine (less)
{
"name": "appwithoutviewlesscss",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"less-middleware": "~2.2.1",
"morgan": "~1.9.1"
}
}
Notice the dependencies that are included in all cases:
cookie-parser
- Description: Parse
Cookie
header and populatereq.cookies
with an object keyed by the cookie names. Optionally you may enable signed cookie support by passing asecret
string, which assignsreq.secret
so it may be used by other middleware.
- Description: Parse
debug
- Description: A tiny JavaScript debugging utility modelled after Node.js core's debugging technique. Works in Node.js and web browsers.
- Note: The Express docs actually have a section on debugging that is probably much more useful than consulting the
debug
module docs at the npm site. As noted from the Express docs: Express uses the debug module internally to log information about route matches, middleware functions that are in use, application mode, and the flow of the request-response cycle [e.g., check earlier projects we did and you will seedebug
nestled into thenode_modules
folder].debug
is like an augmented version ofconsole.log
, but unlikeconsole.log
, you don't have to comment outdebug
logs in production code. Logging is turned off by default and can be conditionally turned on by using theDEBUG
environment variable. - Additional note: If you want to see all of the internal logs used in Express when testing/building your application, you could add the following line to the
scripts
object in your projectpackage.json
:"debugStart": "DEBUG=express:* nodemon login-site-sending-files-emphasis.js"
. And when you wanted to see everything logged so you have thorough debugging capability and to see what all Express is logging, you could executenpm run debugStart
from within your project directory, effectively executing the above line. As Express internally uses thedebug
module, you do not have torequire
the debug module in your application to use the script just defined.
express
- Description: Fast, unopinionated, minimalist web framework for node.
morgan
- Description: HTTP request logger middleware for node.js.
- Note: We see
app.use(logger('dev'));
in ourapp.js
. From the docs linked to above formorgan
, we see the following description for thedev
option: Concise output colored by response status for development use. The:status
token will be colored green for success codes, red for server error codes, yellow for client error codes, cyan for redirection codes, and uncolored for information codes. Given the inclusion ofmorgan
and what thedev
option gives, it is unlikely you will want to include something as over the top as thedebugStart
script above.
So what dependencies change from one package.json
to the next based on options supplied to the express generator? Well, whenever a CSS engine is used, of course the appropriate middleware for that engine is loaded as a dependency (how else would the CSS engine run?). And when a view engine is used, we not only get the view engine added as a dependency (e.g., hbs
) but also we get the http-errors
module. Why? [The extremely short answer is that http-errors
gives us more robust capabilities with handling/creating HTTP errors.]
Well, Express gives you a views
directory by default when you use the express generator and one of the files in the views
folder is error.<view-engine-extension>
. This is meant to provide a view for when the user encounters an error such as a 404 or whatever else they might run into. As made clear from for http-errors
, we have a chance to customize the error messages we respond with by using the createError
function and passing such functions to next()
. Why would we call next
with createError
as an argument? The Express docs on error handling explain why: If you pass anything to the next()
function (except the string 'route'
), Express regards the current request as being an error and will skip any remaining non-error handling routing and middleware functions.
What are error-handling routing and middleware functions? Read the docs starting at the section about the default error handler. Essentially, error-handling middleware functions are defined in the same way as other middleware functions except error-handling functions have four arguments instead of three: err
, req
, res
, and next
. For example:
app.use(function (err, req, res, next) {
console.error(err.stack)
res.status(500).send('Something broke!')
})
Additionally, you define error-handling middleware last, after other app.use()
and routes calls; for example:
var bodyParser = require('body-parser')
var methodOverride = require('method-override')
app.use(bodyParser.urlencoded({
extended: true
}))
app.use(bodyParser.json())
app.use(methodOverride())
// start defining error-handling middleware
app.use(function (err, req, res, next) {
// logic
})
It is useful to note that Express comes with a built-in error handler that takes care of any errors that might be encountered in the app. This default error-handling middleware function is added at the end of the middleware function stack. If you pass an error to next()
and you do not handle it in a custom error handler, it will be handled by the built-in error handler; the error will be written to the client with the stack trace. The stack trace is not included in the production environment.
As the docs note, if you call next()
with an error after you have started writing the response (for example, if you encounter an error while streaming the response to the client) the Express default error handler closes the connection and fails the request. So when you add a custom error handler, you must delegate to the default Express error handler, when the headers have already been sent to the client:
function errorHandler (err, req, res, next) {
if (res.headersSent) {
return next(err)
}
res.status(500)
res.render('error', { error: err })
}
As noted in the docs, we can implement the "catch-all" errorHandler
function as follows (as an example):
function errorHandler (err, req, res, next) {
res.status(500)
res.render('error', { error: err })
}
And this example closely reflects the catch-all error handling at the bottom of the boilerplate app.js
given to us by the express generator (with createError
from http-errors
slightly modified to illustrate possible usage):
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404, 'Custom 404 message')); // <-- customize/create HTTP errors, make messages, etc.
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
Per the instructions in the docs, this "catch-all" error handling middleware is defined and used last (i.e., after app.use()
and routes calls).
Finally, the docs give a neat example of how you might want to handle errors in the case of checking whether or not you are dealing with a paid subscriber and whether or not to fetch paid-for content. The basic idea is that if we have a route handler with multiple callback functions, then we can use the 'route'
parameter to skip to the next route handler and shortcircuit any other callbacks being used in the current route:
app.get('/a_route_behind_paywall',
function checkIfPaidSubscriber (req, res, next) {
if (!req.user.hasPaid) {
// continue handling this request
next('route')
} else {
next()
}
}, function getPaidContent (req, res, next) {
PaidContent.find(function (err, doc) {
if (err) return next(err)
res.json(doc)
})
})
In this example. the getPaidContent
handler will be skipped but any remaining handlers in app
for /a_route_behind_paywall
would continue to be executed.
This kind of behavior is well-explained in the docs for how app.METHOD
and router.METHOD
behave concerning their usage of callback functions. For app.METHOD(path, callback [, callback ...])
the docs, along with some commentary in light of our journey, indicate the following concerning the arguments assed to app.METHOD
(the same points apply to router.METHOD
):
path
The path
argument is the path
for which the middleware function is invoked; can be any of:
- A string representing a path. [This is what we have typically done.]
- A path pattern. [This is what we have done when dealing with parameters and the like or query strings.]
- A regular expression pattern to match paths.
- An array of combinations of any of the above.
See path examples for more. The default value for path
is '/'
(i.e., root path) unless otherwise specified.
callback
Callback functions; can be:
- A middleware function. [This is pretty much what we have always done.]
- A series of middleware functions (separated by commas). [Extremely useful to know: small use case presented in
app.get('/a_route_behind_paywall', ...)
example above.] - An array of middleware functions.
- A combination of all of the above.
You can provide multiple callback functions that behave just like middleware, except that these callbacks can invoke next('route')
to bypass the remaining route callback(s). You can use this mechanism to impose pre-conditions on a route, then pass control to subsequent routes if there is no reason to proceed with the current route.
See the middleware callback function examples section for ideas on how to implement more than just the simple use case of a single callback function as we have done for the most part thus far. Better yet, read the entire entry for app.use([path,] callback [, callback...])
as it includes a ton of valuable information.
Looking at the package.json
, we see a "start": "node ./bin/www"
script
. So we shouldn't have to provide a path to our application to start running it. We should be able to simply run nodemon
in the terminal where our application is and all will be right with the world. Why?
According to the usage section of the docs for nodemon
: If you have a package.json
file (which we do!) for your app, you can omit the main script (i.e., www
in the case of the express generator and often app.js
or somethign else in most other cases) entirely and nodemon
will read the package.json
for the main
property and use that value as the app. nodemon
will also search for the scripts.start
property in package.json
(as of nodemon
1.1.x
).
So that last part above is why: nodemon
will simply search for the scripts.start
property in our package.json
and run accordingly. See nodemon
FAQ for more fun details.
HTTP headers: a brief introduction in the context of Express
TLDR: You can set the response's HTTP header field
to value
by using res.set(field [, value])
, as noted in the docs. To set multiple fields at once, simply pass an object as the parameter:
res.set('Content-Type', 'text/plain')
res.set({
'Content-Type': 'text/plain',
'Content-Length': '123',
ETag: '12345'
})
That said, let's revisit something we discussed at the beginning of the notes, namely the request-response cycle and the fact that each request or response is an HTTP message, where the message is made up of three things:
- Start line
- Header
- Body
Recall that the start line is a single line, and it describes the type of request on the way there, and on the way back in the response it's the status:
- Request:
method
|path
|version-of-HTTP
; for example:get
|/blog
|HTTP/1.1
- Response:
version-of-HTTP
|status-code
; for example:HTTP/1.1
|404
Recall that the headers basically describe the incoming(request)/outgoing(response) body (i.e., content). So essentially the headers contain metadata. The header always comes in the form of key-value pairs (can be thought of in JS almost like JSON). There will always be a blank line between the header and body. And that is to indicate that all of the header is done and that it's time for the body.
Recall that the body is the "actual stuff" or what you may think of as maybe the content or HTML, the image, etc.
All of the above is what makes up HTTP messages. You have to follow that protocol (Hyper Text Transfer PROTOCOL), namely having a start line, header, and body. You can only control these things. It's up to others (i.e., browsers) to follow the protocol as well and to make sense of what you are trying to send. But note that how the browser behaves is almost completely determined by the headers. The body is what we work hard to put together for the user, but it's really rather useless if the headers are a mess and don't accurately describe the body. For example, suppose we want to send JSON or an image back to the user but somehow/somewhere we have set the content-type
header to be text/plain
. That's going to be rather useless for the user because the browser, presumably because it is following the protocol, is not going to make sense of our body because one of the headers is out of whack.
The point of all this is that anytime you do res.
: json
, render
, send
, sendFile
, download
, redirect
, etc., you are not actually sending back JSON, a webpage, a file, or anything else to the user but an HTTP message. That is what Express does. It handles HTTP traffic via Node.js. And the HTTP message is what we described above: a start line, headers, and the body. As noted above, you do not have control over what the other side is going to do with that HTTP message. Just because you send back a content-type
of text/html
with a body of a bunch of parseable HTML does not mean that whoever's on the other end has to make a web page out of it. It could be curl
and curl
could just print it off. It could be a browser that has decided it is not going to follow the rules, and it's going to do something else with it entirely. Whatever the case, all you can do is make sure you follow the rules, and the rules are what actually make up the Protocol in Hyper Text Transfer Protocol. All the browsers have agreed to follow the rules that are laid out in how HTTP messages will work so that when you send back your HTTP message, if you follow the rules, then there's a common understanding of how everything should work. That is how the game works.
The start line is fairly straightforward, the body is fairly straightforward, but most of the important stuff is located in the headers. The headers are really really important to understand, and they can be rather intimidating since there are so many of them, but generally speaking they're fairly intuitive.
At the risk of beating it to death, know that on the other end you have something else (a user, a server, etc.) sending a request to you (doesn't matter who's sending you the request so long as the request is an HTTP request), and HTTP is built on the request sent to your Express server. Express ingests that request, you have your middleware which will do its thing (you'll modify the request object, modify the response object, run a bunch of logic, decide what you're going to do, etc., and all of that is now your responsibility as a developer). In the end, Express will send back a response and remember that this response is only an HTTP message: a start line, headers, and a body. Whoever's on the other side can do whatever they want with the response. If they want to honor your headers, the body, etc., then they can or they don't have to. You have no control over that. What you can do is follow the rules about what's inside your headers so that whoever's on the other end who made the requests knows what they're supposed to do with the data, body, or whatever you sent back.
Express has made it easier for us in dealing with a lot of common headers by including a lot of header-specific methods on the request
and response
objects. We will not be doing stuff with the whole gamut of HTTP headers--the goal is to get some practice, exposure, and confidence in dealing with HTTP headers because you're going to have to interact with them at some point. Maybe not often but definitely occasionally.
As usual, the MDN section on HTTP headers is comprehensive and will be a great reference tool. As MDN notes, headers can typically be grouped into 4 types according to their contexts:
- General headers apply to both requests and responses, but with no relation to the data transmitted in the body.
- Request headers contain more information about the resource to be fetched, or about the client requesting the resource.
- Response headers hold additional information about the response, like its location or about the server providing it.
- Entity headers contain information about the body of the resource, like its content length or MIME type.
And headers can also be grouped according to how proxies handle them:
We'll start by looking at a good example of a general header that applies to both requests and responses and certainly has no relation to the data being transmitted in the body: the data. Using the application generated by the express generator in the previous note, we will execute the following in the terminal after we get our server listening using nodemon
: curl -v localhost:3000
. We get the following at this time of writing:
* Rebuilt URL to: localhost:3000/
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: text/html; charset=utf-8
< Content-Length: 207
< ETag: W/"cf-sMq3uu/Hzh7Qc54TveG8DxiBA2U"
< Date: Fri, 17 Apr 2020 19:13:49 GMT
< Connection: keep-alive
<
<!DOCTYPE html>
<html>
<head>
<title>Express</title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1>Express</h1>
<p>Welcome to Express</p>
</body>
</html>
* Connection #0 to host localhost left intact
Specifically notice the Date header:
< Date: Fri, 17 Apr 2020 19:13:49 GMT
Since we sent a request to the root, let's go into our index.js
file in routes
(which handles requests to the root of our domain), and see if we can modify the Date header somehow.
In general, the quickest and easiest way to change a header in Express is the res.set
. Of course, res.get
is its corresponding partner. That is how you get the value of a header. Everything else we go over is Express trying to make things easier for us, but res.set
is how you set headers the fast and obvious way. We could change the header like so for our route handling GET
requests to the home page (i.e., the root):
/* GET home page. */
router.get('/', function(req, res, next) {
const date = new Date(1969, 6);
res.set('Date', date)
res.render('index', { title: 'Express' });
});
And if we send another curl request, then we'll get back
< Date: Tue Jul 01 1969 00:00:00 GMT-0500 (Central Daylight Time)
Clearly the wrong date! But we have the power to change it now. Notice we also have
< Content-Type: text/html; charset=utf-8
If we want to change this header as well, we can do something like res.set('Content-Type', 'text/plain');
to alter the Content-Type
header. Note that this will cause the agent receiving our response to interpret the body differently because we have now described the body as only consisting of plain text via text/plain
instead of text/html
(so it will not be parsed as HTML):
/* GET home page. */
router.get('/', function(req, res, next) {
const date = new Date(1969, 6);
res.set('Date', date);
res.set('Content-Type', 'text/plain');
res.render('index', { title: 'Express' });
});
Now here's where things get very interesting! If you send another curl request to the root then we will get back
< Content-Type: text/plain; charset=utf-8
as one of the headers which is expected. But if we hop back over to the browser and send a request to the homepage, then nothing changes. It looks the exact same. It still looks like HTML is being interpreted somehow when we just wanted it to be plain text. What's going on here? Welcome to caching! Real quick, here's the opening paragraph to the Wiki article on what a cache is in computing:
From wiki: In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere. A cache hit
occurs when the requested data can be found in a cache, while a cache miss
occurs when it cannot. Cache hits are served by reading data from the cache, which is faster than recomputing a result or reading from a slower data store; thus, the more requests that can be served from the cache, the faster the system performs.
As the next paragraph notes: To be cost-effective and to enable efficient use of data, caches must be relatively small. Nevertheless, caches have proven themselves in many areas of computing [...].
Cool! Well, does Chrome use caching? Of course it does. So us not seeing a change in the page view (of the homepage) despite our manual change of a header (the Content-Type
header) is really just the browser trying to do its job. Chrome is caching our page. In order to break this for the moment, you will need to Inspect
(get to the dev tools) and then right-click on the refresh wheel and click "Empty Cache and Hard Reload". This will clear the headers out. The browser is preserving the old headers assuming they haven't changed because why would the Content-Type
of the home page change? How often does that happen? Well, in development, it happens all the time, but in the real world it almost never happens.
So now we actually get the plain text we expected! We now have the power to change the headers to whatever we want.
If we look at the general header article on MDN, we will see that the most common general headers are Date, Cache-Control, and Connection. We already discussed Date
, but Cache-Control
is going to be our homerun. This is where we will spend the most time as an example. And then we will pass briefly over the other things Express does with headers. Remember, the goal is to just get some practice, exposure, and confidence in dealing with headers. Then we can roam freely.
Before delving into Cache-Control, let's briefly review what caching is all about. If you go to a website and you load it up, then you go through the whole request-response cycle process from beginning to end and you (hopefully) end up with the data you were requesting. If, say 5 minutes later, you come back to the website you just got data from a moment ago and load up the exact same page, if nothing has changed, then there's no reason for you to bog down your computer and the server and the network with everything that goes into that request-response cycle. Of course, your own computer is probably quite fast, and the Internet is also very fast. So would it really make a difference? Probably not. But high-octane sites like google.com and facebook.com are trying to shave the slightest nanosecond off of anything that they can because it might save them millions of dollars. Allegedly, Google once deminified their homepage so there were return carriages, spaces, and so on in the actual HTML, and it cost them 7 or 8 figures just from that tiny little change. So caching is incredibly important, but it's also one of those topics that's incredibly complicated so as a junior or mid-level developer you probably won't have a lot of control over it, but it is very very important and Express gives us a few options to deal with it.
As the MDN article notes, Cache-Control
is a general header and caching directives are unidirectional. So just because Cache-Control
may be set going one way, it does not mean it will be set the same way when coming back from another direction (i.e., Cache-Control
for request may be different from Cache-Control
for response and vice-versa). The caching directives are case-insensitive and there are different cache directives based on whether or not you are dealing with the request or the response (meaning Cache-Control
will always
be the key
but what you are allowed to use as the directive or "value" for this key depends on whether or not you are dealing with a request or response).
For example, as MDN notes, standard Cache-Control
directives that can be used by the client in an HTTP request are as follows:
Cache-Control: max-age=<seconds>
Cache-Control: max-stale[=<seconds>]
Cache-Control: min-fresh=<seconds>
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: no-transform
Cache-Control: only-if-cached
And standard Cache-Control
directives that can be used by the server in an HTTP response:
Cache-Control: must-revalidate
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: no-transform
Cache-Control: public
Cache-Control: private
Cache-Control: proxy-revalidate
Cache-Control: max-age=<seconds>
Cache-Control: s-maxage=<seconds>
The max-age
directive is the requestor saying, "I will use a cached resource for this long and only this long." To understand max-stale
, let's issue a curl request to Google: curl -v google.com
. You will see something like the following as one of the response headers:
< Expires: Sun, 17 May 2020 19:54:19 GMT
So, once we pass the above date (whatever date is on the Expires
header if there is one), the response is considered stale. If the client is willing to accept something that is stale, then max-stale[=<seconds>]
indicates how far past stale (in seconds) the requestor is willing to go. So, for example, the requestor could say, "I'll accept a stale resource (web page, JSON, image, whatever) for a few seconds, an hour, or a day." If you're really excited about the caching stuff, you can check out the detailed protocol on W3. min-fresh=<seconds>
would be, "It has to be at least this fresh." no-cache
means the requestor is going to expect some kind of validation to make sure that it doesn't want to use a cache unless there's absolutely no reason for it to get a new response. no-store
means the requestor will not store anything about the response. It will not keep any data--this is not efficient and we'll look at it in just a moment. If you look further down the MDN page, they have a brief paragraph about each directive.
On the part of the response, it has must-revalidate
which is just what it says and it also has no-cache
and no-store
directives. There are also public
and private
directives. So think about how you are on the other side now. You are the server. And if you have a file that is a bunch of Node.js and it's always the same (say like a template file built with EJS), and instead of compiling that EJS every single time, if it's pretty much always the same, then why wouldn't you just run it one time, cache the resulting HTML, CSS, and JavaScript, and then send that cache every single time? Problem solved! Now you don't have to do all of the extra work every single time. Well, sometimes you are going to have private stuff. You will have stuff in the response that is unique to that user, and you can specify that as private
, meaning, "I can cache this for this user, but I can't use this copy for everybody else (that is what public
would be for)." There's also a max-age=<seconds>
and s-maxage=<seconds>
.
Let's return to the no-store
directive real quick, which we can apply as a Cache-Control
directive for either request or response. If we go back to our index.js
and again change res.set('Content-Type', 'text/plain');
to res.set('Content-Type', 'text/html');
and reload, then nothing will happen (because the browser is still caching our page by default). But if we include res.set('Cache-Control', 'no-store');
as well (and then clear cache and hard reset to clear the headers out and reload), then we can freely change back and forth between res.set('Content-Type', 'text/plain');
and res.set('Content-Type', 'text/html');
and see the change immediately reflected because there is no cached copy of our page to even use anymore. This is good for development and only for development! But this can be a really really nice thing when you're working on a project and you can't figure out why you're constantly fighting the cache. You can simply set it to no-store
and it will tell the browser through the Cache-Control
header that it does not want it to store anything. Of course, we used the no-store
directive on the response
object because we're in charge of the response--we're not actually initiating the request (the browser is).
Two other directives we can quickly get out of the way are fresh
and stale
, and these belong to the request
object, meaning the requestor is letting you know how old a thing is. The Express docs on req.fresh
and req.stale
say it best:
req.fresh: When the response is still "fresh" in the client's cache, true
is returned, otherwise false
is returned to indicate that the client cache is now stale and the full response should be sent. When a client sends the Cache-Control: no-cache
request header to indicate an end-to-end reload request, this module will return false
to make handling these requests transparent. Further details for how cache validation works can be found in the HTTP/1.1 Caching Specification. Example use case:
console.dir(req.fresh)
// => true
req.stale: Indicates whether the request is "stale," and is the opposite of req.fresh
(so we should have req.stale === !req.fresh
). For more information, see req.fresh
. Example use case:
console.dir(req.stale)
// => true
Since req.fresh
and req.stale
are booleans, you could run different logic based on the cache by utilizing these different booleans.
This finishes up the meaty portion concerning HTTP headers. The rest touched on below will be just a brief accounting for.
req.ips
: There is a header calledx-Forwarded-For
, and this is used whenever someone is behind a proxy to be able to find them. That is, if someone is behind a router or someone is behind a load balancing server or something like that, that means that you as the server will not be able to see their real IP address. You'll see the IP address of their proxy, and then the proxy will get them to their actual IP address. The router can send thex-Forwarded-For
, which is the actual IP address of that client machine.req.ips
will contain an array with all of those IPs. If we look atapp.set()
, which contains the settings table for our Express application, then you can see thattrust proxy
property is set tofalse
(disabled) by default and even includes a note for our benefit: NOTE:X-Forwarded-*
headers are easily spoofed and the detected IP addresses are unreliable. So you would only modifytrust proxy
if you really knew what you were doing with it and wanted to modify its behavior.req.xhr
: This is a boolean property that will be set to true if theX-Requested-With
header is an XMLHttpRequest. Essentially what that means is: "Is this an AJAX request? Did axios, jQuery, a front-end site, or generally some library client make an AJAX request to the server? Again, you can't always trust this becauseX-Requested-With
can be spoofed, but it's the general way to accomplish that.req.accepts(types)
: You pass it a string and that string will be aContent-Type
(e.g.,html
,json
,application/json
etc.), and if that content type is accepted by the requestor it will be returned. There is anAccept
HTTP header field, and we can see this if we usecurl -v google.com
again. So in our request, sent by curl, we'll see> Accept: */*
as one of the headers. Essentially what this means is that we're willing to accept anything. Text, image, json, etc. We don't care. We'll accept anything. But if we want to send back HTML and we're not sure if the requestor is accepting HTML, then we can check by executingreq.accepts(html)
to make sure. This will either respond withhtml
to indicate a confirmation/validation or it will returnfalse
. We can test some of this out using Postman (such a valuable tool!). We can send aGET
request tohttp://localhost:3000
and specify the request headers before sending the request. Often we haveAccepts: */*
meaning we'll accept anything as noted above, but this time we can change it toAccepts: application/json
and then modify ourindex.js
homepage route handler forGET
requests like so:
/* GET home page. */
router.get('/', function(req, res, next) {
const date = new Date(1969, 6);
console.log(req.accepts('html')); // false
console.log(req.accepts('application/json')); // application/json
res.set('Date', date);
res.set('Content-Type', 'text/html');
res.set('Cache-Control', 'no-store');
res.render('index', { title: 'Express' });
});
We can also pass req.accepts
an array of content types we want to test to see if they're accepting any of them. And as the docs note, if we pass a list or array, then the req.accepts
method returns the best match (if any).
So this is how req.accepts
works. There's also the following:
req.acceptsCharsets(charset [, ...])
: Returns the first accepted charset of the specified character sets, based on the request'sAccept-Charset
HTTP header field. If none of the specified charsets is accepted, returnsfalse
.req.acceptsEncodings(encoding [, ...])
: Returns the first accepted encoding of the specified encodings, based on the request'sAccept-Encoding
HTTP header field. If none of the specified encodings is accepted, returnsfalse
.req.acceptsLanguages(lang [, ...])
: Returns the first accepted language of the specified languages, based on the request'sAccept-Language
HTTP header field. If none of the specified languages is accepted, returnsfalse
.
For all of these, we are told to visit accepts
for more information (or if we have issues or concerns when using any of the above three methods on the request object).
The point is that all three of the methods above do as they say. For example, we can check if utf-8
is being accepted or English as a language, etc.
req.get(field)
: Returns the specified HTTP request header field (case-insensitive match). TheReferrer
andReferer
fields are interchangeable. If a header is undefined, then you will simply get backundefined
. Otherwise you will get the value (e.g.,req.get('Content-Type')
might give youtext/plain
or something like that). This is basically the counterpart/partner ofreq.set
.req.range(size[, options])
: This will parse out theRange
header. AndRange
is a little bit tricky because what you're doing is asking for maybe portions of the document and it will usually be in bytes, but you can read through the docs andreq.range
will try to do the heavy lifting for the parsing if you want to do that.
We can also check out some of the properties on the response object related to HTTP headers:
res.append(field [, value])
: Appends the specifiedvalue
to the HTTP response headerfield
. You just want to add something to a givenfield
. For example, previously we looked at something likereq.accepts('application/json')
. But maybe later we wanted to check to see if the requestor is acceptinghtml
and we didn't want to redefine anything or manipulatereq.accepts('application/json')
--we could dores.append(accepts, 'html')
. A very common one is for whenLink
is the field (where its values always come in an array). So basicallyres.append
is ultimately for headers (i.e.,field
s) that have array-likevalue
s and you want to add something on to it.res.format(object)
: This is kind of cool but probably use-case specific in terms of whether or not you will find it to be of any use. Basically, it usesreq.accepts
to find out what type of content the request is willing to accept and then you can run a specific callback based on this almost like aswitch
statement:
res.format({
'text/plain': function () {
res.send('hey')
},
'text/html': function () {
res.send('<p>hey</p>')
},
'application/json': function () {
res.send({ message: 'hey' })
},
default: function () {
// log the request and respond with 406
res.status(406).send('Not Acceptable')
}
})
So if the user is accepting text/plain
run the given function, etc. Basically, as the docs note, res.format
performs content-negotiation on the Accept
HTTP header on the request object, when present. This is pretty cool but not all that commonly used.
res.get(field)
: Just likereq.get(field)
,res.get(field)
will return the value of any response header. So you could do something likeres.get('Content-Type')
if you need to know what theContent-Type
is at a given point in the request-response cycle, and that can be really handy in a piece of middleware. If you don't know what the final response is going to send back, then you can grab the header, and it can be really helpful.res.links(links)
: This is just a really fast way of making theLink
header. This joins thelinks
provided as properties of the parameter to populate the response'sLink
HTTP header field. For example, the call
res.links({
next: 'http://api.example.com/users?page=2',
last: 'http://api.example.com/users?page=5'
})
yields the following results:
Link: <http://api.example.com/users?page=2>; rel="next",
<http://api.example.com/users?page=5>; rel="last"
Usage of the Links
header is common for APIs that limit "page size" for how many things can be fetched at once. For example, this Game of Thrones API sends back a Link
header to indicate the pageSize
requested, what the next page could be, what the last one would be, etc.
res.location(path)
: This sets the responseLocation
header to the specifiedpath
parameter. So if you useres.redirect([status,] path)
for a redirection, then you could simply passres.location(path)
as thepath
variable inres.redirect
and write theLocation
header also while providing the desiredpath
destination for the redirect.res.sendStatus(statusCode)
: Sets the response HTTP status code tostatusCode
and send its string representation as the response body. Express is already going to write this stuff for you, but if you wanted to do it yourself for some reason, then you have that power.
res.sendStatus(200) // equivalent to res.status(200).send('OK')
res.sendStatus(403) // equivalent to res.status(403).send('Forbidden')
res.sendStatus(404) // equivalent to res.status(404).send('Not Found')
res.sendStatus(500) // equivalent to res.status(500).send('Internal Server Error')
res.status(code)
: Sets the HTTP status for the response but does not send the status code's string representation as the response body as is the case withres.sendStatus(code)
. Note the examples below how we set the HTTP status for the response but we end up sending something other than the string representation for the status code back as the response.
res.status(403).end()
res.status(400).send('Bad Request')
res.status(404).sendFile('/absolute/path/to/404.png')
res.type(type)
: This will explicitly setContent-Type
HTTP header to the MIME type as determined by mime.lookup() for the specifiedtype
. Essentially it is a quicker way of writingres.set('Content-Type', type)
(with all of the included benefits ofmime.lookup()
as well), and presumablyres.type
is provided because setting theContent-Type
is such a common thing to do:
res.type('.html')
// => 'text/html'
res.type('html')
// => 'text/html'
res.type('json')
// => 'application/json'
res.type('application/json')
// => 'application/json'
res.type('png')
// => 'image/png'
res.vary(field)
: Adds the field to theVary
response header if it is not there already (this has to do with the cache):
res.vary('User-Agent').render('docs')
Phew! Brief introduction to headers but that's the gist. Most of this is stuff you are never going to have to deal with, but the point is that now you are enlightened and should not be afraid to deal with it if needed. If you do somehow find yourself needing to deal with headers, then res.set
is your best friend and what you will use most often in Express land.
Remember: You do not send back web pages, JSON, or anything like that. You send back HTTP messages. And what to do with the HTTP message, whether you're the requestor or the responder, is always going to be determined by what's in the headers.