Express documentation

Express is a Node package which is a web development framework for creating servers using Node.

What is Express?

Express helps us;

Installing Express

npm i express

Don't forget to require express in the JS file.

How to start a server with Express

  1. Require Express
  2. const express = require('express');
  3. Execute Express
  4. const app = express();
  5. Handle incoming requests with the use method
  6. app.use((req, res) => {
    CODE TO RUN WHEN WE GET INCOMING REQUESTS GOES HERE SUCH AS;
    res.send();
    })
  7. Listen for requests at a particular port number, below example is port 3000.
  8. app.listen(3000, () => {
    CODE TO RUN WHEN WE START LISTENING GOES HERE
    })

Request and Response objects

For every method in Express, Express automatically creates 2 javascript objects for us to use, Request and Response

The req object is based upon the incoming HTTP request.

The res object is used to generate a response to send back to the person who requested it.

Express routing basics

Express routing refers to taking incoming requests and a path that is requested, and matching that to some code and some response. For example, a path can be www.url.com/pathname and serving up that page.

app.get('/', (req, res) => {
res.send('This is the homepage');
})

app.get('/shop', (req, res) => {
res.send('This is the shop page');
})

// Below sets a get request for all requests, good for 404 pages. Make sure to add at the end.
app.get('*', (req, res) => {
res.send('This is a 404 page');
})

Express path paremeters

We can set paremeters using the : in the path to send the right pages/data to the user.

app.get('/r/:subreddit', (req, res) => {
const { subreddit } = req.params;
res.send('Browsing the ${subreddit} subreddit');
})

Working with query strings

Express creates an object called query within the request object. The query object will hold key value pairs based on the query string.

app.get('/search', (req, res) => {
const { q } = req.query;
if (!q) {
res.send('Nothing found');
}
res.send('Search results for ${q}');
})

Auto-restart with Nodemon

To restart the server automatically with Nodemon we need to use that when starting the server instead of Node in the terminal.

nodemon THE APP NAME

Templating with EJS

What is templating

Templates allows us to define a preset 'pattern' for a webpage, that we can dynamically modify. Same as in Elementor, when we create a template such as a header, footer blog page or shop page.

We can create logic in the HTML for JS to create certain HTML elements in certain states, such as sign in or sign out buttons

Installing EJS

  1. In the terminal;

    npm i ejs
  2. Then make a folder called 'views' in the project directory that Express will look for.
  3. In the views folder, make a file called 'home.ejs'
  4. Then we can write our EJS in the home.ejs file

To include EJS in express

In the index.js file;

app.set('view engine', 'ejs');

Adding webpages to express routes

  1. First, we need to have all ejs files within the views folder.
  2. in the index.js file;
    app.get('/', (req, res) => {
    res.render('home');
    })

Setting the views directory

In index.js

const path = require('path');
app.set('views', path.join(__dirname, '/views'));

Basic EJS syntax

Below outputs the javascript value into the HTML

<p> Hello world <%= 4 + 5 + 1 %> </p>
Outputs => Hello world 10

Passing data to Templates

The best practice is to write the javascript logic in the js files not the ejs files, then to just reference them in the ejs files to keep the ejs file clean.

app.get('/random', (req, res) => {
const num = Math.floor(Math.random() * 10) + 1;
res.render('random',
rand : num
});

Then we reference rand in the ejs file

Conditionals/Loops in EJS

To add conditional JS in the HTML, we us the tags below. These tags will not render anything on the page, but allows us to add logic and then add html within this to render different things based on the logic written.

<% CODE HERE %>

Serving static assets in Express

To include static assets such as css files, js files, images etc, we need to serve them within the index.js file and then link them in the html file.

Partials in EJS

Partials allows us to add templates within templates, such as having a header partial and including the header in all of the pages.

To add partials in EJS;

  1. Create a partials folder in the public folder.
  2. Create partial files
  3. In pages where we want to add a partial, add;
    <%- include('file path') %>

EJS Mate NPM Package

EJS Mate is an NPM package that can help us add partials more easily

Instead of creating partials and referencing header, nav, footer etc in every page, EJS Mate does the opposite. You create the boilerplate in boilerplate.ejs where you reference the body, then in every page, just include the layout function and start coding

  1. Require EJS Mate
  2. Set the engine
    app.engine('ejs', ejsMate)
  3. In views directory, define a new folder called layouts
  4. In the layouts folder, create a file, boilerplate.ejs
  5. In the boilerplate.ejs in the body tag, reference the body
    <%- body %>
  6. In another template, Remove everything (including body tags) apart from the body and at the top, add;
    <% layout('layouts/boilerplate.ejs') %>
  7. In the views directory, create a partials folder.
  8. In the partials folder, add partials such as navbar.ejs
  9. In boilerplate.ejs, include our partial
    <%- include('../partials/navbar.ejs') %>

Defining RESTful routes

Get vs Post requests

Get Requests

Post Requests

Parsing the request body

When handling post requests we need to tell express what type of post data to expect to parse. Without this, express will not know how to parse the data.

  1. Form data
  2. app.use(express.urlencoded({ extended: true }));
  3. JSON data
  4. app.use(express.json());

Intro to REST

REST is a set of guidelines for how a client & server should communicate and perform CRUD operations on a given resource.

The main idea of REST is treating data on the server-side as resources that can be CRUDed

The most common way of approaching REST is in formatting the URLS and HTTP verbs in applications.

One way to follow the REST guidelines is;

GET /comments - list all comments
POST /comments - create a new comment
GET /comments/:id - view one comment (by ID)
PATCH /comments/:id - update a comment
DELETE /comments/:id - delete a comment

Setting up get index route (To show a list of data)

  1. Add a GET request to /comments in index.js that renders the index.ejs file
  2. Create an index.ejs file in the views folder
  3. In the index.ejs file, loop over the resource. For example, loop over all comments.

Setting up the create/new routes (To add data to a list of data)

We need to set up 2 routes, one (new route) will serve the form to create new comments.

  1. Make a NEW route
    1. Add a GET request to /comments/new in index.js that renders the new.ejs file
    2. Create an new.ejs file in the views folder
    3. Make sure we tell express to parse form data
    4. In the new.ejs file, add a form
    5. In the form;
      1. Change the action to /comments
      2. Change the method to POST
      3. Add name and id for each input to reference in the post route
  2. Make a POST route
    1. Add a POST request to /comments in index.js that ...
      1. Extract the form data from the request body
      2. const { username, comment, id: uuidv4() } = req.body:
      3. Push the new data to the database
      4. Redirect the user back to the list of comments from the form page
      5. res.redirect('/comments');

Setting up a show route (To show an expanded view of an item in a list, eg. an AirBNB listing)

  1. Add a GET request to /comments/:id
  2. Take the ID from the URL
  3. const { id } = req.params:
  4. Find the correct comment and parse the number if its a number
  5. const comment = comments.find(c => c.id === parseInt(id));
  6. Render the show page with expanded details such as an AirBNB listing and pass through the comment ID so we can use it within the show.ejs file.
  7. res.render('comments/show', { comment });
    • Make a show.ejs file in the views folder.
    • Add relevant HTML within the show.ejs file to create the show page.

The UUID package

The UUID package allows us to create unique IDs automatically for all user generated comments or posts

  1. Install the UUID package npm i uuid
  2. Require the UUID package in the index.js file
    const { v4: uuidv4 } = require('uuid');
  3. Add the function below to where we want to create a new id, such as in hardcoded comments or when we post new comments with the comments.push
  4. uuidv4();

How to update comments

  1. Add a PATCH request to /comments/:id
  2. Take the ID from the URL
  3. const { id } = req.params:
  4. Save the new comment that is sent in the req.body payload to a variable
  5. const newCommentText = req.body.comment;
  6. Find the correct comment
  7. const foundComment = comments.find(c => c.id === id);
  8. Update comment to new comment text
  9. foundComment.comment = newCommentText;
  10. Redirect the user back to the list of comments from the form page
  11. res.redirect('/comments');

After the above, we need to create a form for the user to update their comment. The issue is HTML forms can only send GET or POST requests, so we need to fake it so send a PATCH request.

  1. Add a GET request to /comments/:id/edit
  2. Take the ID from the URL
  3. const { id } = req.params:
  4. Find the correct comment based on ID in the URL
  5. const comment = comments.find(c => c.id === id);
  6. Render the form page and pass through the comment in the form for the user to edit.
  7. res.render('comments/edit', { comment };
  8. Create edit.ejs file in the views/comments folder.
  9. Create the form inside of the edit.ejs file.

Finally, to get the HTML form to accept a PATCH request, we need to;

  1. Install method overide package.
  2. Override the method using a query string value
    1. Require Method Override
    2. const methodOverride = require('method-override');
    3. Call the app.use function to use the middleware
    4. app.use(methodOverride('_method'));
    5. In the HTML forms action attribute, add the following to the end (make sure the default method of form is set to POST)
    6. ?_method=PATCH

How to delete comments

  1. Add a DELETE request to /comments/:id
  2. Take the ID from the URL
  3. const { id } = req.params:
  4. Filter all comments that is not the one we want to delete and filter it into a new array (We do this when not working with databases)
  5. comments = comments.filter(c => c.id !== id);
  6. Redirect the user back to the list of comments
  7. res.redirect('/comments');
  8. Add delete button where needed by;
    1. Adding a from with a method of POST
    2. Change the action attribute to /comments/:id?_method=DELETE to override the POST request to DELETE
    3. Add the button inside the form

Express Middleware

Express Middleware are functions that run during the request/response lifecycle

Middleware

Morgan - Logger Middleware

Morgan helps us log HTTP request information to our terminal which is very useful when debugging.

To install Morgan

  1. Install from NPM
  2. Require Morgan
  3. Execute Morgan using app.use

Defining our own Middleware

We use the Next keyword to tell the Middleware to go to the next thing.

app.use((req, res, next) => {
console.log('This is my first Middleware')
return next()
})

Setting up a 404 route

  1. Add a res.use function at the end of the express app (before the .listen) - 404 page will run if the request does not match anything else.
  2. In the function, write - res.status(404).render() - this will set status code to 404 and render 404 page

Adding Middleware to specific routes

If we need to add Middleware to specific routes such as verifying passwords, instead of using app.use we can create functions and save as a variable, then pass the variable into the .GET or other routes after defining the route but before defining the function for that route

Handling errors in Express

Express' built in error handler

Express error handler is something built into Express to handle errors and send back an internal error response.

We can throw our own errors

throw new Error()

Defining custom error handlers

We can define error handling middleware functions, just like any other middleware, just with 1 distinction, error handling have four functions instead of three (err, req, res, next)

We put error handling middleware last, after all the other app.use functions

If we do not pass any argument into next() then the next available code will run, if we pass an argument into next(err) then only the next error handling middleware will run.

app.use((err, req, res, next) => {
const = {status = 500, message = 'Something went wrong'} = err
console.error(err.stack)
res.status(status).send(message)
return next(err)
})

Our custom error class

  1. Make a new file such as AppError.js
  2. class AppError extends Error {
    constructor(status, message) {
    super()
    this.message = message
    this.status = status
    }
    }

    module.exports = AppError
  3. In the app.js
    1. const AppError = require('./AppError')
  4. Set up an error handling route
    1. app.use((err, req, res, next) => {
      const = {status = 500, message = 'Something went wrong'} = err
      res.status(status).send(message)
      return next(err)
      })
  5. Where we need to throw error
    1. throw new AppError(401, 'password required')

Handling Async Errors

Below is for when we want to force errors such as when the user is not logged in or needs a password

To handle errors in Async functions we need to pass the error to the next error handler instead of handling the error directly in the function

app.get('/', async (req, res, next) => {
return next(new AppError(404, 'Product not found'));
})

// need to have an error middleware at the bottom of the file to pass the error to.

Below is for when users may cause errors such as not filling in all required information

We can use the try/catch method

app.get('/', async (req, res, next) => {
try {
// code goes here
} catch (err)
next(err)
}
})

When we want to throw an error within a try/catch, we can just add throw new instead of return next

We typically want to wrap everything in try/catch anytime we use an async function as Express can usually handle non-async errors but cannot handle async errors by default.

Defining an Async utility

Instead of typing a try/catch over and over again in all of our async functions, we can create a function that passing in another function and chains a .catch so all we have to do then is wrap our async functions in this function instead of writing try/catch over and over.

Simply, we create a function wrapAsync that returns a new function (whatever we need to write) and adds a .catch

function wrapAsync(fn) {
return function (req, res, next) {
fn (req, res, next).catch(err => next(err))
}
}

app.get('/', wrapAsync(async (req, res, next) => {
// code goes here
}))

Differentiating Mongoose Errors

Mongoose has names for different types of errors, we can take those names and make functions to handle those errors differently.

const handleValidationErr = err => {
console.dir(err)
return new AppError(400, 'Validation Failed ... ${err.message}')
}

app.use((err, req, res, next) => {
console.log(err.name)
if (err.name === 'ValidationError') err = handleValidationErr(err)
next(err)
})

Defining our Error template

  1. Create a file, error.ejs in the views folder.
  2. Create content for error page
  3. Render error page in our error route within app.js

JOI validator tool

Joi is a middleware that helps us validate our data on the server side to throw specific errors easily depending on the data validations we set

  1. Install npm package
  2. Require Joi
  3. Define schema
// Creating Joi Schema
const campgroundSchema = Joi.object({
campground: Joi.object({
title: Joi.string().required(),
price: Joi.number().required().min(0),
image: Joi.string().required(),
location: Joi.string().required(),
description: Joi.string().required()
}).required()
})
const {
error
} = campgroundSchema.validate(req.body)
if (error) {
const msg = error.details.map(el => el.message).join(',')
throw new ExpressError(400, msg)
}

Express Router

Express router allows us to organize our routes and minimize duplication.

  1. Make a folder called Routes
  2. Add a JS for for each router
const express = require('express')
// makes a new router and saves it to a variable
const router = express.Router()

router.get('/posts', (req, res) => {
// Route code goes here
})

module.exports = router

In the index.js file

const postRoutes = require('./routes/posts')
// the '/posts' allows us to name all the routes so we do not have to repeatedly type them in every route.
app.use('/posts', postRoutes)

We can add middleware to apply to only certain routes by writing them with router.use within the single js file ofr the routes. For example, require authentication for admin routes.

Cookies

Cookies are little bits of data (key value pairs) that are saved in someones browser during their session and could be sent to the sites server for things such as persionalizaton (their favourite preferences, or if they are logged in, or whats in their shopping cart) or tracking.

Sending cookies

We can send cookies to a users browser and store information in their browser

app.get('/setname', (req, res) => {
res.cookie('name', 'Henriette')
})

Cookie parser middleware

We need to get the cookie information out of the req.body and parse the information to use it.

  1. Install the cookie parser
    npm i cookie-parser
  2. Require cookie parser
    const cookieParser = require('cookie-parser')
  3. Execute cookie parser
    app.use(cookieParser())

After the above is done, on every incoming request, we will have a property called req.cookies so we can access all cookie information to use in our app from here.

Signing cookies

Signing cookies is a way to verify the informations integrity and that it has not changed. Making sure that the original data that was sent to the browser, is the same data being sent back to the server.

We send a signed cookie by setting signed to true and access the signed cookies sent back to the server by using req.signedCookies

// we need to pass a secret key through
app.use(cookieParser('secretKey'))

// send a signed cookie
app.get('/getsignedcookie', (req, res) => {
res.cookie('fruit', 'grape', { signed: true })
})

// acessing and verifying the signed cookied sent back to the server (signed cookied become unsigned and ready to use when sent back)
app.get('/verifysignedcookie', (req, res) => {
console.log(req.signedCookies)
})

HMAC signing

HMAC = Hash based message authentication code

HMAC signing takes the cookie value and secret and re-signs to check if it has the same output as the original cookie to see if any values have been changed and make sure the integrity of the cookie is intact.

Express sessions & Flash

Introduction to sessions

It's not very practical or secure to store a lot of data client-side using cookies.This is where sessions comes in.

Sessions is a server side data store that we use to make HTTP stateful. Instead of using cookies, we store the data on the server-side and then send the browser a cookie that can be used to retrieve the data.

Installing Express Sessions

  1. Install NPM package express-session
  2. Require it
  3. Add a secret so sessions can sign the cookies
    app.use(session({ secret: 'thisisnotagoodsecret'}))
  4. Then we have access to req.session to use in our app
  5. We can add anything to req.session and that data will be stored in the data store.

Introduction to Flash

A place in a session to flash a message to a user. Such as a success or confirmation message.

  1. Install and require connect-flash
  2. After the sessions code add app.use(flash())
  3. Now we have access to req.flash to use for flash messages.
  4. We add req.flash messages before the redirect and on the next page, we pass in the message. Or use res.locals middleware.

List of sources

    Colt Steeles web developer bootcamp 2021