In this tutorial, you'll learn how to build a simple and clean Node.js server-side rendered application using the Express framework and Pug templates styled with CSS.
As a bonus, you'll learn how to streamline your Node.js development workflow by using
nodemon
to restart the server and browser-sync
to reload the browser whenever relevant source files change.Look for the 🛠️️ emoji if you'd like to skim through the content while focusing on the build steps.
What You Will Build
You'll build a login portal for a restaurant named WHATABYTE using server-side rendering (SSR):
We tested this tutorial using Node.js
v12.16.0
and npm v6.13.4
. If you need to install Node.js and npm, use any of the official Node.js installers provided for your operating system.Bootstrap a Node.js Project
🛠️ Create a project directory named
whatabyte-portal
anywhere in your system and make it your current directory:mkdir whatabyte-portal cd whatabyte-portal
🛠️ Execute the following command within the
whatabyte-portal
directory to initialize your Node.js project with default settings:npm init -y
🛠️ Then, create the entry point of the application, a file named
index.js
:touch index.js
Create an npm script to run the application
You'll use
to monitor your project source code and automatically restart your Node.js server whenever it changes. nodemon
🛠️ As such, install
nodemon
as a development dependency:npm i -D nodemon
The command above is the equivalent of running
.npm install --save-dev nodemon
🛠️ Create a
dev
script command in your package.json
file to run nodemon
and delete the test
script:{ "name": "whatabyte-portal", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "nodemon ./index.js" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "nodemon": "^2.0.4" } }
nodemon
gets as an argument the entry point of an application and executes it.Testing Node.js applications is out of the scope of this tutorial. However, it's important to implement testing to create robust, production-ready web apps.
You now have the basic structure of a Node.js application.
Bonus: Prepare for Git Version Control
🛠️ Create a
file under the project directory to avoid committing unnecessary or sensitive files. Populate that file with the content of this Gist or feel free to use gitignore.io to create your own custom .gitignore
.gitignore
file.Set Up Express with Node.js
🛠️ To use the Express framework in your application, install it as a project dependency:
npm i express
🛠️ Open
index.js
and populate it with the following template that defines the core structure of an Express application:// index.js /** * Required External Modules */ /** * App Variables */ /** * App Configuration */ /** * Routes Definitions */ /** * Server Activation */
🛠️ Now, under the
Required External Modules
section, import the express
and path
packages:// index.js /** * Required External Modules */ const express = require("express"); const path = require("path");
The
module provides you with utilities for working with file and directory paths. You'll use it later to create cross-platform file paths to access view templates and static assets, such as stylesheets and images.path
🛠️ Next, under the
App Variables
section, add the following:// index.js /** * App Variables */ const app = express(); const port = process.env.PORT || "8000";
Here, you execute the default function exported by the
express
module to create an instance of an Express application, which you then store in app
. You also define the port the server will use to listen for requests: its value is process.env.PORT
, if available, or 8000
by default.🛠️ Under the
Routes Definitions
section, create a simple route handler for the HTTP GET /
request that replies with a string.// index.js /** * Routes Definitions */ app.get("/", (req, res) => { res.status(200).send("WHATABYTE: Food For Devs"); });
Refer to Express Basic Routing for more details.
🛠️ Finally, under the
Server Activation
section, start a server listening for incoming requests on port
and to display a message to confirm it's listening:/** * Server Activation */ app.listen(port, () => { console.log(`Listening to requests on http://localhost:${port}`); });
Run an Express application
🛠️ Execute
dev
to test the script and run the app:npm run dev
🛠️ Visit
to see the app in action. The browser should display the string "WHATABYTE: Food For Devs" on a plain page.http://localhost:8000/
Use the Pug Template Engine with Express
Modern web apps are data-driven and need views that can display data dynamically. While you can create good-looking views using static HTML templates, you can't hydrate them with data from the server.
Instead, you use template engines like Pug to create dynamic views that can render UI elements conditionally and that you can hydrate with values from the server. What makes Pug stand out from other offerings is its concise syntax and its support for template inheritance to compose pages easily.
🛠️ To use Pug, start by installing the
pug
package using another terminal window:npm i pug
To optimize page composition through templates, you'll use template inheritance to extend a core layout template that encapsulates the top-level HTML structure of a page.
🛠️ As such, create a directory named
views
under the project directory to store all of your templates.mkdir views
Under this new directory, create a
layout.pug
file:touch views/layout.pug
Populate
views/layout.pug
with the following content:block variables doctype html html head meta(charset="utf-8") link(rel="shortcut icon", href="/favicon.ico") meta(name="viewport", content="width=device-width, initial-scale=1, shrink-to-fit=no") meta(name="theme-color", content="#000000") title #{title} | WHATABYTE body div#root block layout-content
Notice that this template uses the
title
variable to render the document title of a page. This variable value will be passed from the server to the template by route handlers defined in the API, also known as controllers.🛠️ Next, create an
index.pug
file under the same directory:touch views/index.pug
Add the following content to
views/index.pug
:extends layout block layout-content div.View h1.Banner WHATABYTE div.Message div.Title h3 Making the Best h1 Food For Devs span.Details Access the WHATABYTE Team Portal div.NavButtons a(href="/user") div.NavButton Just dive in!
With these two templates, you are using the
block
and extends
artifacts to implement template inheritance. Within a template like layout.pug
, you can define content injection points through a named block
. Another template, such as index.pug
, can then inject content into that template by first extending it and then defining the content of the named block.Pug templates don't use HTML tags. Instead, they simply use the names of HTML elements and whitespace to define their structure. You establish parent-child relationships between elements using indentation.
You can define element attributes, such as
charset
, by placing the attribute key and value next to the element name and enclosing it in parentheses, resembling the structure of a function call:meta(charset="utf-8")
The content of an element can go next to the element name in the same line or indented below:
h1.Banner WHATABYTE div.Message div.Title h3 Making the Best h1 Food For Devs
Visit Getting Started for a complete overview of how Pug works.
To connect the templates with the controllers, you need to configure Express to use Pug as the view template engine.
🛠️ Open
index.js
and add the following under the App Configuration
section:// index.js /** * App Configuration */ app.set("views", path.join(__dirname, "views")); app.set("view engine", "pug");
Here, you use
to assign a setting name to a value. Some setting names are reserved by Express to configure the behavior of the app, such as app.set(name, value)
views
and views engine
.The
views
setting tells Express what directory it should use as the source of view template files. In this case, you set the views
directory as the source using the path.join()
method, which creates a cross-platform file path. The
view engine
setting tells Express what template engine to use, which in this case, is pug
.Render Pug views as client responses
🛠️ Next, you'll refactor the
GET /
controller to make it render index.pug
as the client response. Under the Routes Definitions
section, update your route handler as follows:// index.js /** * Routes Definitions */ app.get("/", (req, res) => { res.render("index", { title: "Home" }); });
The first argument of
is a string representing the file path of a template, relative to the templates source directory, res.render(view)
views
. It renders the file and sends the rendered HTML as a string to the client. The extension for the template file defaults to .pug
as Pug is the default view engine.Its second argument is optional and represents an object that the controller passes to the template. The properties of this object become local variables in the template. Therefore,
{ title: "Home" }
defines a local title
variable in index.pug
. However, index.pug
doesn't use title
locally; instead, the template it extends,layout.pug
, uses it.Refer to Using template engines with Express for more details.
🛠️ Refresh the browser to see the new page rendered on the screen.
When you make any changes in the
index.pug
template, you can refresh the browser to see the changes. However, manually refreshing the browser to see updates slows down your development process. To overcome that, you'll use Browsersync.Add Live Reload to Express Using Browsersync
You can emulate the live reload behavior of front-end frameworks such as React and Angular in Express templates using Browsersync. When a CSS rule or the return value of a function is changed, the browser refreshes automatically.
🛠️ Start by installing Browsersync as follows:
npm i -D browser-sync
🛠️ Working with Browsersync is easy. Execute the following command to create a configuration file with default options:
browser-sync init
browser-sync init
creates a bs-config.js
file under your project directory, which contains a large number of default settings. You only need a small subset of those options to use Browsersync to reload your web pages when you change your project's code.🛠️ Replace the content of
bs-config.js
with the following:module.exports = { proxy: "localhost:8000", files: ["**/*.css", "**/*.pug", "**/*.js"], ignore: ["node_modules"], reloadDelay: 10, ui: false, notify: false, port: 3000, };
What are these Browsersync configuration options doing?
proxy: "localhost:8000"
Proxy the Node/Express app served on
localhost:8000
as Browsersync only creates a static server for basic HTML/JS/CSS websites. You still need to run the server with nodemon
separately.files: ["**/*.css", "**/*.pug", "**/*.js"]
Using glob patterns>), specify the file paths to watch. You'll watch CSS, Pug, and JavaScript files.
ignore: ["node_modules"]
Specify the patterns that file watchers need to ignore. You want to ignore
node_modules
to let Browsersync run fast.reloadDelay: 10
Time in milliseconds to delay the reload event following file changes to prevent Nodemon and Browsersync from overlapping, which can cause erratic behavior.
ui: false
Don't start the Browsersync user interface, a page where you can control the behavior of Browsersync.
notify: false
Disable a distracting message notifying you that Browsersync is connected to the browser.
port: 3000
Use a specific port to host the site.
Check the "Browsersync options" document to learn more details about all the configuration options that Browsersync offers you to optimize your development workflow.
🛠️ Next, create an npm script that configures and runs Browsersync to serve your web pages. Open and update
package.json
with the following ui
npm script:{ "name": "whatabyte-portal", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "nodemon ./index.js", "ui": "browser-sync start --config bs-config.js" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": {...}, "dependencies": {...} }
🛠️ To serve the user interface of your Express app, run the following command in a separate terminal window:
npm run ui
The terminal will output information about the local and external locations serving the static site:
[Browsersync] Proxying: http://localhost:8000 [Browsersync] Access URLs: ------------------------------- Local: http://localhost:3000 External: http://<YOUR IP ADDRESS>:3000 ------------------------------- [Browsersync] Watching files...
🛠️ Browsersync automatically opens a new window presenting your interface. If it didn't, open
. Make any change on the http://localhost:3000/
index.pug
file, save it, and watch how the site served on port 3000
updates itself.“Learn how Browsersync lets you streamline your development workflow when creating server-side rendered applications with Node and Express.”
Tweet This
Serve Static Assets with Express
To integrate stylesheets and images into the application, you need to configure Express to access and serve static files from a project directory — a process similar to configuring the render engine.
🛠️ For your application, create a directory named
public
under the project directory to serve this purpose:mkdir public
You'll use
, a built-in middleware function that specifies the directory path from which to serve static assets.express.static()
🛠️ Head back to
index.js
and update the App Configuration
section as follows:// index.js /** * App Configuration */ app.set("views", path.join(__dirname, "views")); app.set("view engine", "pug"); app.use(express.static(path.join(__dirname, "public")));
To mount the
express.static()
middleware function, you use app.use()
. You can now put inside the public
directory any CSS or image files that your application needs to use.Style Express templates using CSS
🛠️ Inside the
public
directory, create a style.css
file:touch public/style.css
🛠️ Populate the content of
public/style.css
as follows:/* public/style.css */ body { background: aqua; }
🛠️ Save your changes. To use this stylesheet, you need to link to it from a template file. Update
layout.pug
as follows:block variables doctype html html head meta(charset="utf-8") link(rel="shortcut icon", href="/favicon.ico") meta(name="viewport", content="width=device-width, initial-scale=1, shrink-to-fit=no") meta(name="theme-color", content="#000000") title #{title} | WHATABYTE link(rel="stylesheet" href="/style.css") body div#root block layout-content
🛠️ Save your changes again. Observe how Browersync reloads the browser and the background color of the home page changes. This is the foundation of styling a Pug template in Express using CSS. You can add as many other rules and stylesheets as you desire.
🛠️ The Pug templates you created have CSS classes in their markup to define their layout and presentation. Replace the content of
style.css
with the following to make use of those classes:Click here to see the content of style.css
.
style.css
@import url("https://fonts.googleapis.com/css?family=Raleway:800|Merriweather+Sans|Share+Tech+Mono"); :root { --ui-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 4px 5px 0 rgba(0, 0, 0, 0.06), 0 1px 10px 0 rgba(0, 0, 0, 0.08); fill: rgba(0, 0, 0, 0.54); --ui-shadow-border: 1px solid rgba(0, 0, 0, 0.14); } * { box-sizing: border-box; } html, body, #root { height: 100%; width: 100%; } body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-tap-highlight-color: rgba(255, 255, 255, 0); } h1, h2, h3 { font-family: "Raleway", sans-serif; text-transform: uppercase; padding: 0; margin: 0; color: #2a3747; } h1 { font-size: 40px; } a { color: inherit; text-decoration: none; cursor: pointer; user-select: none; } #root { display: flex; flex-direction: column; } .View { flex: 1; display: flex; flex-direction: column; justify-content: space-between; align-items: flex-start; height: 100%; width: 100%; padding: 20px; background-size: cover; font-family: "Merriweather Sans", sans-serif; } .Banner { display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; border-radius: 5px; overflow: hidden; background: white; padding: 15px; font-family: "Share Tech Mono", monospace; border-bottom: var(--ui-shadow-border); box-shadow: var(--ui-shadow); } .Message { background: white; padding: 30px; border-bottom: var(--ui-shadow-border); box-shadow: var(--ui-shadow); } .Message > .Title { padding-bottom: 20px; } .Message > .Details { display: flex; flex-direction: column; line-height: 1.5em; } .NavButtons { display: flex; width: 100%; justify-content: space-around; align-items: center; padding: 0 20px; } .NavButton { display: flex; justify-content: center; align-items: center; height: 55px; width: 150px; background: #fa4141; border-radius: 30px; font-size: 16px; font-weight: bold; color: white; text-transform: capitalize; border-bottom: var(--ui-shadow-border); box-shadow: var(--ui-shadow); }
The new stylesheet uses CSS variables using the
var()
function to keep your code DRY. Internet Explorer doesn't support CSS variables currently, but Edge supports them!With this new stylesheet applied, the index page looks much nicer:
“Learn how to style Express templates using Pug along with CSS variables.”
Tweet This
Add images to an Express app
You can place image files within the
public
directory and then reference their relative path from any element where you want to use them. Use an image as the background of the div.View
container in index.pug
.🛠️ To start, right-click on the following image and save it as
food.jpg
inside the public
directory.Photo by Caesar Aldhela on Unsplash
🛠️ Next, open
public/style.css
and append to the file a .WelcomeView
class as follows:.WelcomeView { background: url("food.jpg") left bottom; background-size: cover; }
🛠️ Then, open
views/index.pug
and append this new class to the div.View
container. It should now look like this:extends layout block layout-content div.View.WelcomeView h1.Banner WHATABYTE div.Message div.Title h3 Making the Best h1 Food For Devs span.Details Access the WHATABYTE Team Portal div.NavButtons a(href="/user") div.NavButton Just dive in!
Don't forget that indentation is critical for proper template structure with Pug. Indenting creates a child HTML element.
Based on the configuration of this Express app, the
url
value of the background
property references the image using a file path relative to the public
directory. You also specify the image position, left bottom
, and its presentation, cover
, which prevents stretching.You should now have this snazzy image as the background of the index page.
🛠️ Next, save this icon file as
favicon.ico
under the public
directory to get a logo in the browser tab. Hard reload your browser to see this particular change.Create More Views with Pug and Express
🛠️ For practice, create a user profile page. Under the
views
directory, create a Pug template file named user.pug
.touch views/user.pug
Populate this new template with the following:
extends layout block layout-content div.View.UserView h1.Banner Howdy, #{userProfile.nickname}! div.Message div.Title h3 Making Us the Best h1 Teammate Profile pre.Details=JSON.stringify(userProfile, null, 2) div.NavButtons a(href="/logout") div.NavButton Log out
🛠️ The local
userProfile
variable will be provided to this view by the controller that renders it. To create that controller, open index.js
and update the Routes Definitions
section as follows:// index.js /** * Routes Definitions */ app.get("/", (req, res) => { ... }); app.get("/user", (req, res) => { res.render("user", { title: "Profile", userProfile: { nickname: "Auth0" } }); });
Once again, you use the second parameter of
res.render()
to pass data from the controller to the template. In this case, you are passing mock user information to the user profile template.🛠️ Use the
UserView
class defined in the user template to style it. Open public/style.css
and append the following rule:.UserView { background: url("https://cdn.auth0.com/blog/whatabyte-portal/patio.jpeg") left bottom; background-size: cover; }
🛠️ As an alternative to referencing images within the
public
directory, you can use the URL of a hosted image. Visit http://localhost:3000/user
and see the user view rendered with a background image and filled with mock user data.Photo by Helena Lopes on Unsplash.
Recap
You used Node.js, Express, Pug, and CSS to create a web application that renders a stylish user interface with dynamic data by communicating with an API. You are also serving static assets from the server hosting the API.
As a next step, you can learn how to add authentication to Express using Passport.js and Auth0. Securing Node.js applications with Auth0 is easy and brings a lot of great features to the table.
After you sign up for a free Auth0 account, you only need to write a few lines of code to get a solid identity management solution with support for single sign-on, social identity providers (like Facebook, GitHub, Twitter, etc.), and enterprise identity providers (like Active Directory, LDAP, SAML, custom, etc.).
Try out the most powerful authentication platform for free.
Get started →Sounds exciting? ⚡️
Next Step: I'm ready to secure my Express web appI ran into an issueAbout the author
Dan Arias
Staff Developer Advocate
The majority of my engineering work revolves around AWS, React, and Node, but my research and content development involves a wide range of topics such as Golang, performance, and cryptography. Additionally, I am one of the core maintainers of this blog. Running a blog at scale with over 600,000 unique visitors per month is quite challenging!
I was an Auth0 customer before I became an employee, and I've always loved how much easier it is to implement authentication with Auth0. Curious to try it out? Sign up for a free account ⚡️.View profile