After some consideration, I decided that it would be easier to rely on a web framework to assist me in building a web application for Confetti Cuisine. Building custom routes and application logic has become a tedious task, so I’m converting my application to use Express.js.
I still want the application to have home, courses, and sign-up pages. I need to convert the routes to use keywords and syntax found in Express.js. I need to make sure that I serve my static assets out of the public directory and have all necessary package.json configurations set up for launching the application locally. When I feel ready to make this transformation, I’ll start by initializing the project with npm init.
To begin this site redesign, I’m going to create a new project directory called confetti_cuisine and enter that folder. Within the project folder in terminal, I’ll initialize the application package.json with npm init.
Remembering the configurations that I previously set, I’ll keep the default settings for the project name and use entry point main.js.
Now that my package.json is set up, I’m going to add a start script under "scripts", which will allow me to run the application by using npm start instead of node <filename>. I add "start": "node main.js" to my list of scripts.
Don’t forget to separate multiple script items with a comma.
The last step in the initialization process is adding the main Express.js web framework, EJS templating, a layout, and http-status-codes packages to this project. To do so, I run npm install express ejs express-ejs-layouts http-status-codes --save in the command line.
The --save flag saves the express package as a dependency in this project’s package.json. This way, any future work on this project will ensure that Express.js is installed before anything is able to work.
My resulting package.json file looks like the following listing.
{ "name": "confetti_cuisine", "version": "1.0.0", "description": "A site for booking classes for cooking.", "main": "main.js", "scripts": { "test": "echo "Error: no test specified" && exit 1", "start": "node main.js" }, "author": "Jon Wexler", "license": "ISC", "dependencies": { 1 "ejs": "^2.6.1", "express": "^4.16.4", "express-ejs-layouts": "^2.5.0", "http-status-codes": "^1.3.0" } }
Before I add any new files, I’m going to set up my application’s directory structure. The final project structure will look like listing 12.2. I’ll add the following:
. 1 |____main.js |____public | |____css | | |____confetti_cuisine.css | | |____bootstrap.css | |____images | | |____product.jpg | | |____graph.png | | |____cat.jpg | | |____people.jpg | |____js | | |____confettiCuisine.js |____package-lock.json |____package.json |____controllers | |____homeController.js | |____errorController.js |____views | |____index.ejs | |____courses.ejs | |____contact.ejs | |____error.ejs | |____thanks.ejs | |____layout.ejs
Great. Now I’m ready to add application logic.
Now that the application is set up with Express.js installed, I’ll create my main.js application file. Although this file will resemble my http module version, writing it from scratch will eliminate a ton of headaches in converting line by line. My main.js will look like the code in listing 12.3.
The first line of main.js requires the contents of the Express.js package, assigning them to a constant called express. As with the app constant in the first version of this application, I’ll instantiate the express object, representing this project’s main application framework as another constant called app. The app constant will have the ability to set up a GET route, listening for requests made to the root URL (/) and responding with the Express.js send function called on the response. I can finally set up the server to listen on port 3000 and log a message to my console when it’s up and running.
const express = require("express"), 1 app = express(); 2 app.set("port", process.env.PORT || 3000); app.get("/", (req, res) => { 3 res.send("Welcome to Confetti Cuisine!"); }); app.listen(app.get("port"), () => { 4 console.log( `Server running at http://localhost:${app.get( "port" )}` ); });
With this logic in place, I can start the application by running npm start in my command line.
The json and urlencoded Express.js middleware functions will be used as middleware that interpret incoming request bodies for me. In main.js, I’ll add the code in the next listing.
app.use(
express.urlencoded({ 1
extended: false
})
);
app.use(express.json());
Now my application is ready to analyze data within incoming requests. Next, I need to create routes to reach views in my application.
Now that my application has a starting point, I’m going to create routes for the courses and sign-up pages. Additionally, I’ll add a POST route to handle submissions made from the form on the sign-up page.
First, I create a home controller in my controllers folder, which is where I’ll store the functions my routes will use. I need to require this controller by adding const homeController = require("./controllers/homeController") in main.js. I add the code in the next listing to my home controller, below my application’s first route. All three of these functions respond with an EJS page reflecting the requested route. I need to create the following views: courses.ejs, contact.ejs, and thanks.ejs.
exports.showCourses = (req, res) => { 1 res.render("courses"); }; exports.showSignUp = (req, res) => { res.render("contact"); }; exports.postedSignUpForm = (req, res) => { res.render("thanks"); };
In my main.js, I add the following routes and modify my original home-page route to use my home controller too, as shown in listing 12.6. The first route handles GET requests made to view course listings. For the most part, this route behaves similarly to the home page. The route for the contact page also listens for GET requests, as most people will be expecting a sign-up form on this page when the /contact URL is requested. The last route is for POST requests targeting the /contact URL. The GET route is used internally to view who submitted a request to get in contact. The POST route is used by the sign-up form on the contact page.
app.get("/courses", homeController.showCourses); 1 app.get("/contact", homeController.showSignUp); app.post("/contact", homeController.postedSignUpForm);
Now that all the routes are defined, I’m still missing the bulk of the content. It’s time to add and render some views.
With Express.js, my views are going to be cleaner and easier to render. I need to create the views listed in table 12.1.
Filename |
Purpose |
---|---|
layout.ejs | Serves as the application’s main styling and navigation foundation |
index.ejs | Produces the home page’s content |
courses.ejs | Displays course content |
contact.ejs | Displays the contact form |
thanks.ejs | Displays a thank-you message upon form submission |
error.ejs | Displays an error message when a page isn’t found |
I’ll start by generating my application’s layout view, which will handle what the navigation and general site structure looks like from page to page.
For the layout to work, I need to include it in my main application file, right below my initialization of the Express.js module, as shown in listing 12.7. First, I require the express-ejs-layouts module to allow me to use the layout.ejs file. Then, I set the application server to use the ejs rendering template. Last, I set the application server to use the recently required layouts module. This way, when a new view is rendered, it goes through the layout.ejs file.
const layouts = require("express-ejs-layouts"); 1 app.set("view engine", "ejs"); 2 app.use(layouts); 3
I’ll add this file, called layout.ejs, to my views folder. The key component of this file includes <%- body %>, which will be replaced by my target route’s rendered content.
Each of the following views will use this layout to provide visual consistency (and to avoid repetition of code between files). Within the views folder, I’m going to create index.ejs, courses.ejs, contact.ejs, thanks.ejs, and error.ejs files. Like the layout file, these views render as Embedded JavaScript, allowing me to dynamically feed content to the pages from the server file. After creating index.ejs, I change my home-page route (/) to render the index page in place of sending plain text.
The one view I need to focus on is contact.ejs, where I’ll have prospective students fill out a form, submitting a POST request to my application’s /sign-up route. That form will look like the HTML in the next listing. Notice that the form action is /contact and the form method is POST. When the form is submitted, it will make a POST request to the /contact route.
<form action="/contact" method="post"> <label for="name">Name</label> <input type="text" name="name"> <label for="email">Email</label> <input type="email" name="email"> <input type="submit" value="Submit"> </form> 1
I should be all set. If I named my routes to match and render their corresponding views, I should be able to launch my app and see those views rendered within the layout I built. The only thing missing is my app’s ability to serve images and other static files, which I cover next.
The layout file is rendered on every page I visit. Try adding new HTML content above and below the <%- body %>marker. Notice that these elements are applied to every page.
In my first take of this application using http, serving static assets became a big mess. With every new asset I added to my project directory, I needed to create a new route and handle it appropriately. Luckily, Express.js handles this task nicely, requiring virtually no effort on my part to handle any and all static files I want my application to serve. To enable static assets, I’ll use Express.js’ static function by adding app.use(express.static ("public")) below the initialization of Express.js in my application file. This addition allows individual assets in the application to be served directly.
The last major step in converting the app to an Express.js app is using dynamic content in the views.
Confetti Cuisine often modifies its course listings, so the application is better off not showing those courses on a static web page. With Express.js, passing content from the server logic to the view is a piece of cake.
For this app, I need to display an array of course offerings as a JavaScript object. Then I can send the object to my rendered view. I add the code in listing 12.9 to homeController.js. By assigning the courses variable to an array of JavaScript objects, I can use this list and target specific keys in my view. The res.render method allows me to pass the courses object to the view and refer to it as offeredCourses on that page.
Within the view, I can access this array by using the variable name offeredCourses. Within the home controller, that array goes by the name courses.
var courses = [ { title: "Event Driven Cakes", cost: 50 }, { title: "Asynchronous Artichoke", cost: 25 }, { title: "Object Oriented Orange Juice", cost: 10 } ]; 1 exports.showCourses = (req, res) => { res.render("courses", { offeredCourses: courses 2 }); };
To benefit from this feature, I need to add some EJS and HTML to loop through the offeredCourses list in courses.ejs and print the relevant content, as shown in listing 12.10.
<h1>Our Courses</h1> <% offeredCourses.forEach(course => { %> 1 <h5> <%= course.title %> </h5> <span>$ <%= course.cost %> </span> <% }); %>
Now the application is complete. My courses page looks like figure 12.1. Instead of modifying my courses.ejs view every time a modification is made to the course offerings, I can change the array in my main application file. Running the application is the easy part now.
I should anticipate that things won’t go exactly as planned, so it’s always smart to prepare for certain errors and handle them accordingly. Soon, when this array of courses is replaced by contents from a persistent database, I won’t need to make any code changes to update the course listing.
An application handling most expected outcomes ensures a fairly consistent and good experience for its users. I know that my application may be missing some foolproof logic, though, and I prefer to send my own custom error messages to my client’s audience when those errors occur.
For error handling, I’ll create an error controller, errorController.js, to store my functions, as shown in listing 12.11. The first function handles all requests not previously handled, which fits the category of URLs visited without an active route and results in a 404 error, serving error.ejs. The last function handles any internal server errors that occur. Instead of necessarily crashing and scaring the audience away, I prefer a friendlier message.
const httpStatus = require("http-status-codes"); exports.pageNotFoundError = (req, res) => { 1 let errorCode = httpStatus.NOT_FOUND; res.status(errorCode); res.render("error"); }; exports.internalServerError = (error, req, res, next) => { 2 let errorCode = httpStatus.INTERNAL_SERVER_ERROR; console.log(`ERROR occurred: ${error.stack}`) res.status(errorCode); res.send(`${errorCode} | Sorry, our application is taking a nap!`); };
Then I add routes to correspond to these functions. I’ll add the routes in listing 12.12 to trigger the functions in my error controller if no proceeding routes respond to a request.
The order of routes matters. These routes must go below any preexisting routes, as they act as a catch-all and override any routes below them.
app.use(errorController.pageNotFoundError); 1 app.use(errorController.internalServerError);
I need to require this controller by adding const errorController = require("./ controllers/errorController") to the top of my main.js file. Now my application is ready to handle errors and launch. When a URL is visited without a corresponding route, users see my cat, Hendrix, relaxing on the error page (figure 12.2).
Through this project, I redefined the Node.js project file structure to fit a web framework. I used npm to install three external packages. Then I rebuilt the main application file, using Express.js syntax. To create a path for specific URLs, I set up new routes, using Express.js keywords. For a consistent user interface, I used layouts with EJS. Using Express.js’ static library, I set up static assets to be served to the client through my public folder. Last, I added content to the main project application file and set up that content to be served dynamically to one of my views.
With consistent practice of these techniques and proper error handling, I can use Express.js to build future applications in a few steps. With new features such as layouts and dynamic content, I can try to send content to other views in my application or try modifying the layout as it’s used throughout the app.
In unit 3, I discuss how to organize application code around persistent data with Express.js.