Lesson 6. Writing better routes and serving external files

In lesson 5, you directed URL traffic with a routing system that matched request URLs to custom responses. In this lesson, you learn how to serve whole HTML files and assets such as client-side JavaScript, CSS, and images. Say goodbye to plain-text responses. At the end of the lesson, you improve your route code and place your logic in its own module for cleaner organization.

This lesson covers

  • Serving entire HTML files by using the fs module
  • Serving static assets
  • Creating a router module
Consider this

It’s time to build a basic recipe website. The site should have three static pages with some images and styling. You quickly realize that all the applications you’ve built so far respond only with individual lines of HTML. How do you respond with rich content for each page without cluttering your main application file?

Using only the tools that came with your Node.js installation, you can serve HTML files from your project directory. You can create three individual pages with pure HTML and no longer need to place your HTML in main.js.

6.1. Serving static files with the fs module

With the goal of building a three-page static site, using these HTML snippets can get cumbersome and clutter your main.js file. Instead, build an HTML file that you’ll use in future responses. This file lives within the same project directory as your server. See the project file structure in figure 6.1. In this application structure, all content you want to show the user goes in the views folder, and all the code determining which content you show goes in the main.js file.

Figure 6.1. Application structure with views

The reason you’re adding your HTML files to the views folder is twofold: All your HTML pages will be organized in one place. This convention is used by the web frameworks that you’ll learn about in unit 2.

Follow these steps:

  1. Create a new project folder called serve_html.
  2. Within that folder, create a blank main.js file.
  3. Create another folder called views within serve_html.
  4. Within views, create an index.html file.

Add the HTML boilerplate code in the next listing to main.html.

Listing 6.1. Boilerplate HTML for the index.html page
<!DOCTYPE html>
<html>                       1
  <head>
    <meta charset="utf-8">
    <title>Home Page</title>
  </head>
  <body>
    <h1>Welcome!</h1>
  </body>
</html>

  • 1 Add a basic HTML structure to your views.
Note

This book isn’t about teaching HTML or CSS. For this example, I’ve provided some basic HTML to use, but for future examples, I won’t provide the HTML so that I can get to the important stuff more quickly.

The client can see this page rendered in a browser only with the help of another Node.js core module: fs, which interacts with the filesystem on behalf of your application. Through the fs module, your server can access and read your index.html. You’re going to call the fs.readFile method within an http server in your project’s main.js file, as shown in listing 6.2.

First, require the fs module into a constant such as http. With the fs constant, you can specify a particular file in the relative directory (in this case, a file called index.html within the views folder). Then create a routeMap to pair routes with files on your server.

Next, locate and read the file contents of the file in your route mapping. fs.readFile returns any potential errors that may have occurred and the file’s contents in two separate parameters: error and data. Last, use that data value as the response body being returned to the client.

Listing 6.2. Using the fs module in server responses in main.js
const port = 3000,
  http = require("http"),
  httpStatus = require("http-status-codes"),
  fs = require("fs");                                     1
const routeMap = {                                        2
  "/": "views/index.html"
};

http
  .createServer((req, res) => {
    res.writeHead(httpStatus.OK, {
      "Content-Type": "text/html"
    });
    if (routeMap[req.url]) {
      fs.readFile(routeMap[req.url], (error, data) => {   3
        res.write(data);                                  4
        res.end();
      });
    } else {
      res.end("<h1>Sorry, not found.</h1>");
    }
  })
  .listen(port);
console.log(`The server has started and is listening
 on port number: ${port}`);

  • 1 Require the fs module.
  • 2 Set up route mapping for HTML files.
  • 3 Read the contents of the mapped file.
  • 4 Respond with file contents.
Note

When files on your computer are being read, the files could be corrupt, unreadable, or missing. Your code doesn’t necessarily know any of this before it executes, so if something goes wrong, you should expect an error as the first parameter in the callback function.

Run this file by entering this project’s directory on your command line and entering node main.js. When you access http://localhost:3000, you should see your index.html page being rendered. Your simple route guides the response of any other URL extension requested to the Sorry, not found message.

Tip

If you don’t see the index.html file being rendered, make sure that all the files are in the correct folders. Also, don’t forget to spell-check!

In the following example, you serve only the files specified in the URL of the request. If someone visits http://localhost:3000/sample.html, your code grabs the request’s URL, /sample.html, and appends it to views to create one string: views/sample.html. Routes designed this way can look for files dynamically based on the user’s request. Try rewriting your server to look like the code in listing 6.3. Create a new getViewUrl function to take the request’s URL and interpolate it into a view’s file path. If someone visits the /index path, for example, getViewUrl returns views/index.html. Next, replace the hardcoded filename in fs.readFile with the results from the call to getViewUrl. If the file doesn’t exist in the views folder, this command will fail, responding with an error message and httpStatus.NOT_FOUND code. If there is no error, you pass the data from the read file to the client.

Listing 6.3. Using fs and routing to dynamically read and serve files in main.js
const getViewUrl = (url) => {                  1
  return `views${url}.html`;
};

http.createServer((req, res) => {
  let viewUrl = getViewUrl(req.url);           2
  fs.readFile(viewUrl, (error, data) => {      3
    if (error) {                               4
      res.writeHead(httpStatus.NOT_FOUND);
      res.write("<h1>FILE NOT FOUND</h1>");
    } else {                                   5
      res.writeHead(httpStatus.OK, {
      "Content-Type": "text/html"
      });
      res.write(data);
    }
    res.end();
  });
})
.listen(port);
console.log(`The server has started and is listening on port number:
 ${port}`);

  • 1 Create a function to interpolate the URL into the file path.
  • 2 Get the file-path string.
  • 3 Interpolate the request URL into your fs file search.
  • 4 Handle errors with a 404 response code.
  • 5 Respond with file contents.
Note

String interpolation in ES6 allows you to insert some text, number, or function results by using the ${} syntax. Through this new syntax, you can more easily concatenate strings and other data types.

Now you should be able to access http://localhost:3000/index, and your server will look for the URL at views/index.

Warning

You’ll need to handle any and all errors that may occur as requests come in, because there will likely be requests made for files that don’t exist.

Add your new HTML files to your views folder, and try to access them by using their filenames as the URL. The problem now is that the index.html file isn’t the only file you want to serve. Because the response body depends heavily on the request, you also need better routing. By the end of this lesson, you’ll implement the design pattern laid out in figure 6.2.

Figure 6.2. Server routing logic to render views

Quick check 6.1

Q1:

What happens if you try to read a file that doesn’t exist on your computer?

QC 6.1 answer

1:

If you try to read a file that doesn’t exist on your computer, the fs module passes an error in its callback. How you handle that error is up to you. You can have it crash your application or simply log it to your console.

 

6.2. Serving assets

Your application’s assets are the images, stylesheets, and JavaScript that work alongside your views on the client side. Like your HTML files, these file types, such as .jpg and .css, need their own routes to be served by your application.

To start this process, create a public folder at your project’s root directory, and move all your assets there. Within the public folder, create a folder each for images, css, and js, and move each asset into its respective folder. By this point, your file structure should look like figure 6.3.

Figure 6.3. Arranging your assets so they’re easier to separate and serve

Now that your application structure is organized, refine your routes to better match your goal in listing 6.4. This code may appear to be overwhelming, but all you’re doing is moving the file-reading logic into its own function and adding if statements to handle specific file-type requests.

Upon receiving a request, save the request’s URL in a variable url. With each condition, check url to see whether it contains a file’s extension or mime type. Customize the response’s content type to reflect the file being served. Call your own customReadFile function at the bottom of main.js to reduce repeated code. The last function uses fs.readFile to look for a file by the name requested, writes a response with that file’s data, and logs any messages to your console.

Notice that in the first route, you’re checking whether the URL contains .html; if it does, you try to read a file with the same name as the URL. You further abstract your routes by moving the code to read the file into its own readFile function. You need to check for specific file types, set the response headers, and pass the file path and response object to this method. With only a handful of dynamic routes, you’re now prepared to respond to multiple file types.

Listing 6.4. A web server with specific routes for each file in your project
const sendErrorResponse = res => {                   1
  res.writeHead(httpStatus.NOT_FOUND, {
    "Content-Type": "text/html"
  });
  res.write("<h1>File Not Found!</h1>");
  res.end();
};

http
  .createServer((req, res) => {
    let url = req.url;                               2
    if (url.indexOf(".html") !== -1) {               3
      res.writeHead(httpStatus.OK, {
        "Content-Type": "text/html"
      });                                            4
      customReadFile(`./views${url}`, res);          5
    } else if (url.indexOf(".js") !== -1) {
      res.writeHead(httpStatus.OK, {
        "Content-Type": "text/javascript"
      });
      customReadFile(`./public/js${url}`, res);
    } else if (url.indexOf(".css") !== -1) {
      res.writeHead(httpStatus.OK, {
        "Content-Type": "text/css"
      });
      customReadFile(`./public/css${url}`, res);
    } else if (url.indexOf(".png") !== -1) {
      res.writeHead(httpStatus.OK, {
        "Content-Type": "image/png"
      });
      customReadFile(`./public/images${url}`, res);
    } else {
      sendErrorResponse(res);
    }
  })
  .listen(3000);

console.log(`The server is listening on port number: ${port}`);

const customReadFile = (file_path, res) => {           6
  if (fs.existsSync(file_path)) {                      7
    fs.readFile(file_path, (error, data) => {
      if (error) {
        console.log(error);
        sendErrorResponse(res);
        return;
      }
      res.write(data);
      res.end();
    });
  } else {
    sendErrorResponse(res);
  }
};

  • 1 Create an error-handling function.
  • 2 Store the request’s URL in a variable url.
  • 3 Check the URL to see whether it contains a file extension.
  • 4 Customize the response’s content type.
  • 5 Call readFile to read file contents.
  • 6 Look for a file by the name requested.
  • 7 Check whether the file exists.

Now your application can properly handle requests for files that don’t exist. You can visit http://localhost:3000/test.js.html or even http://localhost:3000/test to see the error message! To render the index page with these changes, append the file type to the URL: http://localhost:3000/index.html.

The next section shows you how to further redefine your routing structure and give your routes their own module.

Quick check 6.2

Q1:

What should be your default response if a route isn’t found?

QC 6.2 answer

1:

If your application can’t find a route for some request, you should send back a 404 HTTP status code with a message indicating the page that the client was looking for can’t be found.

 

6.3. Moving your routes to another file

The goal of this section is to make it easier to manage and edit your routes. If all your routes are in an if-else block, when you decide to change or remove a route, that change might affect the others in the block. Also, as your list of routes grows, you’ll find it easier to separate routes based on the HTTP method used. If the /contact path can respond to POST and GET requests, for example, your code will route to the appropriate function as soon as the request’s method is identified.

As the main.js file grows, your ability to filter through all the code you’ve written gets more complicated. You can easily find yourself with hundreds of lines of code representing routes alone!

To alleviate this problem, move your routes into a new file called router.js. Also restructure the way you store and handle your routes. Add the code in listing 6.5 to router.js. In the source code available at manning.com/books/get-programming-with-node-js, this code exists in a new project folder called better_routes.

In this file, you define a routes object to store routes mapped to POST and GET requests. As routes are created in your main.js, they’ll be added to this routes object according to their method type (GET or POST). This object doesn’t need to be accessed outside this file.

Next, create a function called handle to process the route’s callback function. This function accesses the routes object by the request’s HTTP method, using routes[req.method], and then finds the corresponding callback function through the request’s target URL, using [req.url]. If you make a GET request for the /index.html URL path, for example, routes["GET"]["/index.html"] gives you the callback function predefined in your routes object. Last, whatever callback function is found in the routes object is called and passed the request and response so that you can properly respond to the client. If no route is found, respond with httpStatus.NOT_FOUND.

The handle function checks whether an incoming request matches a route in the routes object by its HTTP method and URL; otherwise, it logs an error. Use try-catch to attempt to route the incoming request and handle the error where the application would otherwise crash.

You also define get and post functions and add them to exports so that new routes can be registered from main.js. This way, in main.js you can add new callback associations, such as a /contact.html page, in the routes object by entering get("contact.html", <callback function>).

Listing 6.5. Adding functions to the module’s exports object in router.js
const httpStatus = require("http-status-codes"),
  htmlContentType = {
    "Content-Type": "text/html"
  },
  routes = {                                    1
    "GET": {
      "/info": (req, res) => {
        res.writeHead(httpStatus.OK, {
          "Content-Type": "text/plain"
        })
        res.end("Welcome to the Info Page!")
      }
    },
    'POST': {}
  };

exports.handle = (req, res) => {                2
  try {
    if (routes[req.method][req.url]) {
      routes[req.method][req.url](req, res);
    } else {
      res.writeHead(httpStatus.NOT_FOUND, htmlContentType);
      res.end("<h1>No such file exists</h1>");
    }
  } catch (ex) {
    console.log("error: " + ex);
  }
};

exports.get = (url, action) => {                3
  routes["GET"][url] = action;
};
exports.post = (url, action) => {
  routes["POST"][url] = action;
};

  • 1 Define a routes object to store routes mapped to POST and GET requests.
  • 2 Create a function called handle to process route callback functions.
  • 3 Build get and post functions to register routes from main.js.
Note

More HTTP methods could go here, but you don’t need to worry about those methods until unit 4.

When you call get or post, you need to pass the URL of the route and the function you want to execute when that route is reached. These functions register your routes by adding them to the routes object, where they can be reached and used by the handle function.

Notice that in figure 6.4, the routes object is used internally by the handle, get, and post functions, which are made accessible to other project files through the module’s exports object.

Figure 6.4. The exports object gives other files access to specific functionality.

The last step involves importing router.js into main.js. You complete this the same way you import other modules, with require("./router").

You need to prepend router to every function call you make in main.js, as those functions now belong to the router. You can also import the fs module if you plan to serve assets and static HTML files as before. The code for your server should look like the code in listing 6.6.

With the creation of your server, every request is processed by the handle function in your router module, followed by a callback function. Now you can define your routes by using router.get or router.post to indicate the HTTP method you expect from requests to that route. The second argument is the callback you want to run when a request is received. Create a custom readFile function, called customReadFile, to make your code more reusable. In this function, you try to read the file passed in and respond with the file’s contents.

Listing 6.6. Handling and managing your routes in main.js
const port = 3000,
  http = require("http"),
  httpStatusCodes = require("http-status-codes"),
  router = require("./router"),
  fs = require("fs"),
  plainTextContentType = {
    "Content-Type": "text/plain"
  },
  htmlContentType = {
    "Content-Type": "text/html"
  },
  customReadFile = (file, res) => {                        1
    fs.readFile(`./${file}`, (errors, data) => {
      if (errors) {
        console.log("Error reading the file...");
      }
      res.end(data);
    });
  };

router.get("/", (req, res) => {                            2
  res.writeHead(httpStatusCodes.OK, plainTextContentType);
  res.end("INDEX");
});
router.get("/index.html", (req, res) => {
  res.writeHead(httpStatusCodes.OK, htmlContentType);
  customReadFile("views/index.html", res);
});

router.post("/", (req, res) => {
  res.writeHead(httpStatusCodes.OK, plainTextContentType);
  res.end("POSTED");
});

http.createServer(router.handle).listen(3000);             3
console.log(`The server is listening on port number:
 ${port}`);

  • 1 Create a custom readFile function to reduce code repetition.
  • 2 Register routes with get and post.
  • 3 Handle all requests through router.js.

After adding these changes, restart your Node.js application, and try to access your home page or /index.html route. This project structure follows some of the design patterns used by application frameworks. In unit 2, you learn more about frameworks and see why this type of organization makes your code more efficient and readable.

Quick check 6.3

Q1:

True or false: functions and objects that aren’t added to their module’s exports object are still accessible by other files.

QC 6.3 answer

1:

False. The exports object is intended to allow modules to share functions and objects. If an object isn’t added to a module’s exports object, it remains local to that module, as defined by CommonJS.

 

Summary

In this lesson, you learned how to serve individual files. First, you added the fs module to your application to look for HTML files in your views folder. Then you extended that functionality to application assets. You also learned how to apply your routing system to its own module and selectively register routes from your main application file. In unit 2, I talk about how you can use the application structure provided by Express.js, a Node.js web framework.

Try this

You currently have one route set up to read an HTML file from this lesson’s examples. Try adding new routes in the style introduced in this lesson to load assets.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset