Routing is the process
of deciding how your application should respond to
different types of requests. When a request comes in, your
server needs to decide what function(s) should process that
request. For example:, an app would route a request to a home page
differently than it would route a request to submit
login credentials.
Your code can evaluate the type and URL of the request and
send an appropriate response based on that information.
We would typically not route requests using Node.js
alone: it's not great when you have many different
types of files or want to handle POST requests.
However, we will explore a basic example so you can
better understand how it works (and why we
need frameworks like Express.js)!
Pre-Requisites
Before doing this lesson, make sure you've tried
all the examples in the First
Node.js Apps lesson.
A Simple Route Handler
A lot of programming Node.js programs is
route handling.
This involves taking each request and determining which
function(s) should execute for that request.
We need to learn a bit more before we can do an actual
route handler, but we can simulate a basic one just so
that we can understand route handling in general.
Start a new project (mine is called /route_handler_simple)
and add your app.js file. You can copy and paste the following
code into it:
"use strict";
// example of basic routing, not a "real life" example,
// just to understand how it works
// map of responses for various URL patterns
// - key is the pattern in the URL
// - value is the response we want to give
const routeResponseMap = {
"/routes/info": "<h1>Info Page</h1>",
"/routes/contact": "<h1>Contact Us</h1>",
"/routes/about": "<h1>Learn More About Us.</h1>",
"/routes/hello": "<h1>Say hello by emailing us here</h1>",
"/routes/error": "<h1>Someday I'll Make an Error Page</h1>"
};
const port = 3000,
http = require("http");
http.createServer((req, res) => {
console.log("Received an incoming request!");
console.log(req.url);
// FYI, here's a quick way to set both the status code
// and one or more http headers in a single statement!
res.writeHead(200, {
"Content-Type": "text/html"
});
// see if the url matches any in the mapping table
// (if this key exists in the map = a falsy expression)
if (routeResponseMap[req.url]) {
// found it: send the value as the response body
// included the request URL for debugging
res.write(routeResponseMap[req.url]);
res.end(`<p>Request: ${req.url}</p>`);
} else {
// not found, just generic welcome response
// with the request URL for debugging
res.end(`<h1>Welcome Page</h1><p>Request: ${req.url}</p>`);
}
}).listen(port, () => {
console.log(`Server running on port ${port}`);
});
The first block of code in the file defines a map of
routes as key-value pairs. The key is the request URL
and the value is the heading we will send in the response
body. For any request, we'll look up the request URL in the
routeResponseMap and add the corresponding heading to the
response body.
const routeResponseMap = {
"/routes/info": "<h1>Info Page</h1>",
"/routes/contact": "<h1>Contact Us</h1>",
"/routes/about": "<h1>Learn More About Us.</h1>",
"/routes/hello": "<h1>Say hello by emailing us here</h1>",
"/routes/error": "<h1>Someday I'll Make an Error Page</h1>"
};
Inside the createServer() callback, we log the request to
the console to help us understand what the program is doing:
console.log("Received an incoming request!");
console.log(req.url);
This next block of code in the callback is actually just a
different form of the statements res.statusCode = 200; res.setHeader("Content-Type", "text/html");
In this version, we're calling the response object's
writeHead() method: this method accepts a status
code and a collection of response headers as an object.
In this case, we're only adding one response header so our
object { } contains one key-value pair: "Content-Type" is the
key (the header name) and "text/html" is the value. If you wanted
to add multiple headers, you would separate each key-value
pair with a comma, just like regular JSON syntax.
Next, we check to see if the request URL is one of the
items in our routeResponseMap: if it exists, we add
the appropriate heading (the heading that's the value for the
req.url key) to the response object by using the
res.write() function. Unlike res.end(),
this will only write to the response body: it won't send the response.
In my version, I also added the request URL for debugging
and testing purposes.
The else block will only execute if the request URL was
not one of the items in our routeResponseMap. In this case,
we send a default response "Welcome Page" along with the
request URL.
Try the program: run npm init and fill in
all the values, and then run it with node app
Watch the console while you test the following URLs in your
browser:
localhost:3000/info
localhost:3000/contact
localhost:3000/about
localhost:3000/hello
localhost:3000/error
localhost:3000/foo
localhost:3000/
Each one should show the appropriate heading, although note
that the last 2 will show the Welcome Page heading.
Routing Requests for Files
In the previous example we wrote a web
application that had only one response to any request to
localhost:3000. However, in a real application
you would do a lot more. For example:
if the url is for localhost:3000/info, you want to respond
with the info page
if the url is localhost:3000/login, you want to
respond with the login page
How would this work? You would check the
request's information and then process the request
and send a response based on that information.
Often you would look for patterns in the URL
(e.g. if the URL contains "getyear" or if the
request URL is /info).
We tried to simulate this with the simple route handler we
wrote earlier. However, this program only sent a customized
response containing a heading. Ideally we should
send an appropriate HTML page. We'll do a simple
version of this now.
First, create a new app. I'll call mine /route_handler_better.
Add an app.js file. Add sub-directories /public and /views.
We'll use the public directory to contain any CSS and images
you want in your application's pages. We'll use /views to
store any HTML files that are part of our project.
In the /public directory, add the standards /css and
/images directory. Add a CSS file and some images
to your directories. In the /views directory,
add an index.html and a second .html page.
If you prefer, you can use these files:
route handler views and public directories zip file
(click the Download button on the right beside the "Raw" button).
Now start coding your app.js file:
add the constant variables for the port
number, the http module, and also the file system (fs)
module:
This program is going to use two helper functions:
The first one will read in a file and send it as
a response, or it will send an error response if a
file can't be read. The second function simply encapsulates
the error response code so we can re-use it.
Paste the two functions into your app.js file.
A good review exercise would be to document each statement.
The customReadFile() function accepts the file you want to
read and the response object. We will have to pass the response
object into this function because the function is going to write
to the response body and send it.
The fs.existsSync(path) function checks the file
system to see if path (a directory/file name) exists.
It returns true if it exists and false if it doesn't exist. This
allows us to only fetch files that actually exist.
Inside the fs.existsSync() if block, we read the file using
the fs.readFile() function we became familiar with in the
First Node.js Apps lesson.
In this case, we attempt to read the file and if there
is a problem reading the file, we log an error to the console
and then we invoke the sendErrorResponse() function. I added
a return statement afterwards, which exits the customReadFile()
function at this point.
If there are no problems reading the file, we write the file
to the response object and send the response.
If the file doesn't exist, we invoke the same sendErrorReponse()
function. This function takes the provided response object and
sends a 404 error response to the client.
Now we can set up the server so that it uses the customReadFile()
function whenever a new request comes in. Go ahead and add the
createServer() and listen() functions:
http.createServer((req, res) => {
}).listen(port, () => {
console.log(`The server is listening on port number: ${port}`);
});
Our server will accept a request and then decide which
file to return based on the request URL.
Recall that when a page contains external resources such as
client-side scripts, stylesheets, and images, each resource
triggers a new request to the browser: we will need to handle all
of these requests:
If a file name
ends with .html, we will find the appropriate HTML file
and send it in the response as content-type text/html.
If the file requested ends with .css, we will find the appropriate
CSS file and send it in the response as content-type text/css.
We will do this for each type of file in our project.
Note the image types for the images you chose: the ones I
provided are .jpg images but check your own, also. If you have
.png or .gif, you'll have to add code to handle those.
First, we should set up variables for the path/name of the
file being requested and also the content type header we
will eventually set:
let contentType = "";
let path = "";
Next, we need to extract the file name from the end
of the request URL. We can use split("/") to split the
request URL by the forward slash character; the last
element will be the file name.
let fileName = req.url.split("/").pop();
Now we'll use a if/elseif block to get the correct
path and content type for the file that was requested:
Note that if you have other types of files such as .png or
.gif, you'll need to add an else-if block for those and set the
appropriate content type.
In this if statement, we are ensuring that we set the
path to the correct location of the requested file:
your urls and references in your code may not exactly
match the physical structure, and that's fine.
For example, our stylesheets are in ./public/css but the
<link> element inside the HTML files have the source
as href="css/main.css". This is normal, because
a Node.js project is not public, so it doesn't have the
same directory structure. The path "css/main.css" or even
"./css/main.css" wouldn't work in our project, because all
css files are in the /public directory.
Let's add some console output for debugging purposes:
If we get through this multi-sided if and the contentType
is still empty, it means that we didn't find a matching
route for the requested file, so we will send the error response.
That's it! Now save everything and if run
npm init on your project if you haven't already
done it. Then run your program (node app
or nodemon app.js or npm start).
Everything should work: you should be able to click links,
see the css styling, images, etc.
Now that you've tried this newer version of the routing
program, you can probably still see some issues with it:
What if we have other file types that already part of the
code we wrote (e.g. if you added a .png or .gif, you had to add
else-if blocks for those)? What if our site grows and
we add more kinds of files to it?
What if one of our HTML pages had some client-side JavaScript
in an external .js file? We have to add another else-if block
to respondng with any requested .js file
What if we wanted to do POST requests? POST requests
are used for writing/adding data to a file/database,
for large amounts of data or non-string data (such as objects),
or for sending sensitive form data such as passwords. Plain
Node.js can only handle GET requests, not POST requests.
You can see that this is going to get tedious for even small
projects, but also it's just not enough. We need to be able to
do more, like POST requests, database/file writes, etc.
To fix this, we can use a framework like Express.js.
Express.js can handle POST requests, write to a database or
file, and much more.
This will allow us to use the
MVC
(Model View Controller) design pattern create applications with
dynamic data and much more functionality. This also means we
can write better code for route handling.
Practice Exercises
For each exercise, set up a new project directory.
Run and test each project locally and on your server,
if you have access to one.
Create a new Node.js project. Add four HTML files to the
project root: about.html, contact.html, error.html, and
index.html. Each page has minimal html, a header, and a
footer element. The header contains a level-1 heading with
the page title (the error page should say "Error: Not
Found"). Add an app.js file to your project that contains
code that does the following:
create a map of routes to the about, contact, and index pages
e.g. my project directory is /ex4 so I have /ex4 as the key
for index.html, /ex4/contact as the key for contact.html, etc.
when your server receives a request, get the file name
for the request URL (use your map of routes)
if a request URL doesn't have a route, use the
error.html page
Log all requests to the console for debugging purposes
Test your application and make sure the correct page loads. For
all requests you haven’t mapped, the error page should load.