Students Will Be Able To: |
---|
List the Fundamental Capabilities of Web Frameworks |
Create a Basic Express Web App |
Define Basic Routes |
Respond to HTTP Requests |
Render Dynamic Views Using EJS |
- Setup
- The Three Fundamental Capabilities of Web Frameworks
- Intro to the Express Framework
- Express "Hello World"
- Basic Structure of the Express Server
- Auto-Restart the Server Using Nodemon
- The Route's Callback Function
- Request & Response Objects
- Ways to Respond to a Request
- Rendering Views
- Dynamic Templating using EJS
- Redirecting
- Essential Questions
-
Move into your code folder:
cd ~/code
-
Create a folder and cd into it:
mkdir intro-express cd intro-express
-
Create a
package.json
using this command:npm init
Accept the defaults, except for the entry point - set this to be "server.js".
Npm init is used to create a new node project. Package.json contains metadata about the project.
-
Open the project's folder in your code editor:
code .
Welcome to Full-Stack development!
Unlike last unit where all of the code we wrote ran on the "front-end" (the browser), the code we're about to write in this lesson runs on the "back-end" as depicted by the green box in the following diagram:
Web Application Frameworks such as Express help developers code web applications that run on the back-end.
Regardless of which specific Web Framework we choose to use, they all provide three capabilities fundamental to developing a web application that runs on a server in the cloud:
- The ability to define routes
- The ability to process HTTP requests using middleware
- The ability to use a view engine to render dynamic templates
Over the next few lessons, you will learn about how the Express framework implements these three fundamental capabilities.
Express is the most popular web framework for Node.js.
It is minimalistic and lightweight, especially when compared to massive frameworks like Django and Rails.
Express uses Node's built-in HTTP module to listen for, and respond to, HTTP requests - Express simply adds those three web application capabilities on top of Node.
Let's use npm
to install the Express module in this project:
npm i express
Note that
i
is a shortcut forinstall
Create a server.js
module to put our web app's main code in:
touch server.js
Let's write the obligatory "Hello World!" application:
// Load express
const express = require('express');
// Create our express app
const app = express();
// Define a "root" route directly on app
// Tomorrow, we'll use best practice routing
app.get('/', (req, res) => {
res.send('<h1>Hello World!</h1>');
});
// Tell the app to listen on port 3000
// for HTTP requests from clients
app.listen(3000, () => {
console.log('Listening on port 3000');
});
Run the app:
node server
Browsing to localhost:3000
will hit our app's root route that we defined and return "Hello World!".
Using DevTools, we will find that despite just sending back the text of<h1>Hello World!</h1>
,
the browser "built" a minimal HTML document to display it in.
Using send()
is a general purpose way to respond to the request, however, it's kind of like using console.log()
- soon we'll be using more specific methods.
In server.js
, let's document using comments what a typical Express server needs to do:
// Require modules
const express = require('express');
// Create the Express app
const app = express();
// Configure the app (app.set)
// Mount middleware (app.use)
// Mount routes
app.get('/', (req, res) => {
res.send('<h1>Hello World!</h1>');
});
// Tell the app to listen on port 3000
app.listen(3000, () => {
console.log('Listening on port 3000');
});
Let's make a minor update to our root route:
// Mount routes
app.get('/', (req, res) => {
res.send('<h1>Hello Express</h1>');
});
Refreshing the page will reveal that it didn't work! This is because we have to restart the server, or...
nodemon
is a popular development tool used to automatically restart the Express app when we save changes.
You may have installed it during installfest, however, you can make sure you have the latest version by running:
npm i -g nodemon
Command line tools are installed using the
-g
(global) option
If you received an error during the install, there's a workaround by using:
npx nodemon <module name>
instead of
nodemon <module name>
Now, thanks to the main
entry in package.json
, we can start the server by simply typing nodemon
(or npx nodemon
).
Like most web frameworks, Express uses the HTTP Method
and the Path/Endpoint
of the HTTP request to match a route defined in the application.
In our first route, we defined a route using the get
method on the Express app
object.
The get
method defines a route that listens for a GET
request. There are other methods such as post
, put
and delete
, that map to the other HTTP verbs.
The first argument provided to app.get
, /
, defines the path for the route. In this case the root of the application, i.e., just the host name like localhost:3000
.
In Express, all strings used to define a path should start with a forward-slash character (/
).
In the next Express lesson, we'll learn a preferred way of defining routes using the Express Router
object, but you need to be aware of defining routes this way as well.
The second argument provided to app.get()
is a callback function that is executed by Express when the server receives an HTTP Request that matches the route:
app.get('/', (req, res, next) => {
res.send('<h1>Hello Express</h1>');
});
Making sure to use the proper method is important. In Express, you might have the same endpoint (route) for multiple HTTP methods (GET, POST, PUT, DELETE, etc.). The combination of the route and the HTTP method helps you define different behaviors for the same endpoint based on the type of request.
β What part(s) of the HTTP Request does Express use when determining what route the request matches?
The HTTP Method and the Path/Endpoint
When Express calls the callback function it will provide two objects as arguments...
The callback function defines two parameters conventionally named req
& res
:
-
req
: Represents Express's request object has properties and methods used to access information regarding the current HTTP request, including any data being sent from the browser. -
res
: Represents Express's response object has properties and methods used to end the request/response cycle - like we've done so far using theres.send
method. -
next
: is used to continue the processing of the request. This parameter is particularly relevant when working with middleware functions. Middleware functions in Express are functions that have access to the req and res objects, and they can also modify them or terminate the request-response cycle. Middleware functions are executed in the order they are defined in the application, and they can perform various tasks such as authentication, logging, or modifying the request or response objects.The next parameter is a function that, when called, passes control to the next middleware function in the stack. If a route handler or middleware does not call next(), the request-response cycle might be terminated, and the client might not receive a response.This third parameter is used to continue a request to the next matching route, this is typically used when incorporating middleware functionality like authentication and attaching properties to a request as they enter your web server.
next
will not be talked about much in the introduction to node modules, for right now, just be aware there is a third parameter and it is used to continue a request to a different route. For more information on thenext
parameter consider reviewing this documentation from express on middleware.
-
Define another route that matches a request of
GET /home
and sends a text response of<h1>Home Page</h1>
. -
Test it by browsing to
localhost:3000/home
.
Assuming the following two routes:
app.get('/cars', (req, res) => {
res.send("Here's a list of my cars...");
});
app.post('/cars', (req, res) => {
res.send('Thanks for the new car!');
});
Both routes are defined with the same path of /cars
- is this okay?
Hint: Look closely
Yes, because they defined to match different HTTP Methods which makes those two routes unique.
So far we have responded in our route handler (the callback function) by using the res.send()
method.
The Express docs for the Response object lists the other ways to respond to an HTTP request.
Here are the common methods we'll be using during the course:
res.render()
: Render a view template and send the resulting HTML to the browser.res.redirect()
: Tell the browser to issue anotherGET
request.res.json()
: Send a JSON response (used when we communicate via AJAX).
One of the three fundamental capabilities of a web framework discussed earlier is to be able to use a view engine to render dynamic templates.
A template can include a mixture of static (unchanging) HTML and "code" that generates HTML dynamically.
For example, code in a template could generate a series of <li>
elements for data provided to it in an array.
When a server receives a request, it processes the EJS templates and sends the generated HTML to the client as a response. The client's browser then renders the HTML. EJS itself does not run in the browser; it is a server-side technology.
In Express, we use res.render()
to process a template using a view engine and return the resulting HTML to the browser.
Express can work with a multitude of view engines, including:
Pug
(formerlyJade
) - A template language that leverages indentation to create HTML with a "shorthand" syntax.EJS
(Embedded JavaScript) - A super cool templating language that, like the name says, embed JavaScript within the HTML!
Let's use EJS to render a "home" view for the existing GET /home
route.
A common Express application architecture is to use a design pattern called MVC(Model, view, Controller) This is a pattern based off of MV*, you can read more on MVC and similar MV* design patterns at Mozilla Developer Network MVC documentation. let's put all the templates inside of a folder named views
:
mkdir views
touch views/home.ejs
ejs
is the file extension for the EJS view engine.
Open home.ejs
then type !
and press tab to generate the HTML boilerplate:
<!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">
<title>First Express</title>
</head>
<body>
</body>
</html>
For now, we will need to include the HTML boilerplate inside of every view.
However, EJS includes the ability to make our views more DRY by using partial views. We will cover partial views later, however, if you can't wait, check out how to use the include()
function here.
Update the <title>
as shown above and add an <h1>
inside <body>
so that we see something displayed:
<body>
<h1>Home Page</h1>
</body>
Okay, now let's refactor the GET /home
route's callback to render our new home.ejs
template:
app.get('/home', (req, res) => {
res.render('home');
});
It's convention to specify just the template's name, dropping the ejs
extension.
Browse to localhost:3000/home
and - it doesn't work...
Now's a great time to get a little practice reading Express errors!
The Express error
Error: No default engine was specified...
makes it clear that we need to specify a view engine.
This is our first opportunity to configure the server using Express's app.set()
method:
// Configure the app (app.set)
app.set('view engine', 'ejs');
We also need to inform Express where all of our views can be found:
// Configure the app (app.set)
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
Don't be intimidated by the code:path.join(__dirname, 'views')
...
path.join()
is simply a Node method that builds a properly formatted path from segment strings passed to it.
__dirname
is available in Node modules and represents the path of the current folder where the currently running code lives; and views
is the name of the folder we created to hold our views.
path
is a core Node module, but it still must be required before we can use it.
Core Node modules don't have to be installed with npm install
, but we do have to require
them:
// Require modules
const express = require('express');
const path = require('path');
Refresh and let's see what the next error is...
Error: Cannot find module 'ejs'
tells us that we need to install the EJS view engine:
npm i ejs
We don't need to require
the view engine - Express knows how to find it.
Refresh the page - success π
Thus far, we've only rendered a static template, but now it's time to use EJS to dynamically generate HTML!
In addition to passing the template name as an argument to res.render()
method, we can also pass in a JavaScript object as a second argument and all of its properties will be accessible in the view within ejs
tags!
Let's get to work rendering the list of To Dos...
Normally, the To Dos would be coming from a database, however, we'll "fake" a DB by putting the To Dos in a module and export a method to return them.
Let's create the module:
mkdir data
touch data/todo-db.js
Start with a copy/paste of the following array of To Do objects:
// data/todo-db.js
const todos = [
{todo: 'Feed Dogs', done: true},
{todo: 'Learn Express', done: false},
{todo: 'Buy Milk', done: false}
];
Now let's export a getAll()
method that can be used by any other module to obtain the To Dos:
const getAll = () => {
return todos
}
module.exports = {
getAll
};
To access our To Do "database", we need to require()
it inside of server.js:
const path = require('path');
// require the To Do "database"
const todoDb = require('./data/todo-db');
If we want to be able to implement the "To Do List" functionality, we're going to need another another route:
app.get('/todos', (req, res) => {
const todos = todoDb.getAll()
res.render('todos/index', { todos });
});
As discussed, to pass data to a view, we pass an object as a second argument to res.render()
.
We will now be able to access a todos
variable in the todos/index
view!
It's a best practice to group views related to a data resource such as "todos" in their own folder.
We also commonly use index
as a name for views that render all of something - in this case, displaying all To Dos.
Therefore, we need an index.ejs
view inside of a views/todos
folder:
mkdir views/todos
touch views/todos/index.ejs
Now let's code the todos/index.ejs
view. Start by copying over the HTML from home.ejs
and refactor it to this:
<body>
<h1>All To Dos</h1>
<ul>
<% todos.forEach( todo => { %>
<li>
<%= todo.todo %>
-
<%= todo.done ? 'done' : 'not done' %>
</li>
<% }); %>
</ul>
</body>
That my friends is embedded JavaScript between those <% %>
and <%= %>
tags and I believe you are going to love their simplicity!
The <% %>
EJS tags are for executing JavaScript such as control flow.
The <%= %>
EJS tags are for writing JS expressions into the HTML page.
Refresh and browse to localhost:3000/todos
- yay!
One last bit of fun...
Currently, if we browse to the root route, we see "Hello Express", however, we can use the res.redirect
method to redirect to GET /home
to see the Home page instead.
Refactor the root route as follows:
app.get('/', (req, res) => {
res.redirect('/home');
});
When the server responds with a redirect it causes the browser send a new GET
request to the provided path.
Note: It's very important that the path provided to
res.redirect()
begin with a forward slash!
Certain functionality requires the server to respond using res.redirect()
instead of res.render()
:
- When the browser sends a
GET
HTTP request, the server should respond withres.render()
. - Any request other than a
GET
method, i.e.POST
,PUT
orDELETE
, results in data being changed on the server and typically should be responded to usingres.redirect()
.
(1) Does the Express server "run" on the Back-End or the Front-End?
Back-End
(2) When we define routes on the server, we are mapping/connecting HTTP requests to ________.
Code which performs its purpose such as creating, reading, updating or deleting data, then ultimately responds to the request using res.render()
or res.redirect()
.
(3) Which EJS tags do we use to emit content into the HTML page: <% %>
or <%= %>
?
<%= %>
("Squids", not "Flounders")