Lesson Overview

Often you will need the server to "remember" information about the client in between request/response cycles. Unfortunately, HTTP is stateless, so without extra assistance, this isn't possible. In this lesson you'll learn how you can use Sessions to save information about clients in between different request/response cycles.

Pre-Requisites

Before doing this lesson, it's a good idea to read over and understand how cookies and sessions work.

Sessions in Express

To use sessions, you'll need to install express-session to your project. e.g.

npm install express express-session --save

Once this is installed, you can require it in whatever .js file that needs access to the session data:

const session = require("express-session");

Then you have to add middleware that uses the session object for every request (e.g. to send/receive the session cookie with the session id, start up a new session if none exists, etc.):

const oneDay = 1000 * 60 * 60 * 24;
app.use(sessions({
    // various options
}));

This block of code will execute for all incoming requests: the sessions() middleware function will look for a Session ID in the request: if there is none, it will generate a new Session ID and add it to the response so that it can be sent back to the client. If there is an existing Session ID in the request, it populates the req.session request object property.

The req.session property is a set of key-value pairs. The key is the session variable, where you can store a value or object. The value is what's stored at that particular key.

Session Options

There are a few options you can set to configure your sessions to work the way you like. I'll talk about a few, but there are more in the Express Sessions documentation.

Example:

const oneDay = 1000 * 60 * 60 * 24;
app.use(sessions({
    secret: "secretkeytosessionblahblahblahp983kasjdf;sif",
    saveUninitialized: false,
    cookie: { maxAge: oneDay },
    resave: false 
}));

The req.session Property

The session property is part of the request object. This property allows you access to the session data (variables, objects, etc). The req.session also has an .id property (e.g. req.session.id) which allows you read-only access to the Session ID. Another way to access the session ID is via the req.sessionID property.

All session data can be accessed by key: When you store something in the session store, you assign a unique key (like a variable name or index) to the value/object you want to store. You can access these key-value pairs as req.session properties. For example:
req.session.foo = "bar"
stores the string "bar" into the foo variable of the session object.
if (req.session.foo) { ... }
will execute a block of code if req.session.foo is empty or undefined.

Destroying a Session

The destroy() (e.g. req.session.destroy()) method will destroy all of a session's data. Note that this doesn't destroy the session cookie on the user's machine: that still exists. But that's fine: there would be no matching session for that cookie anymore, anyway. The cookie will eventually get deleted when the browser closes or the cookie expires when no new requests are made.

You might invoke the destroy() function as part of a Log Out process. You can pass destroy() a callback that executes once the session is destroyed.

Sessions Example

Let's try an example. Start a new project and add the usual directories such as /views and /controllers.

In the /views directory, add index.ejs:

<!doctype html>
<html lang="en">
            
<head>
  <meta charset="utf-8">
  <title>Index Page</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="css/main.css">
</head>
            
  <body>
    <header>
      <h1>Index Page</h1>
                
      <% if (username) { %>
        <h2>Welcome, <%= username %>!</h2>
      <% } else { %>
        <h2>Welcome!</h2>
      <% } %>
    </header>
            
    <main>
               
      <% if (!username) { %>
        <form action="/getuser" method="post">
          <div><label for="name">Name:
          <input type="text" id="name" name="username" required>
          </label></div>
          <div><input type="submit" value="Submit"></div>
        </form>
      <% } %>
            
      <p><a href="/page2">Page 2</a></p>
    </main>
            
    <footer>
      <address>&copy; 2023 Wendi Jollymore</address>
    </footer>
  </body>
            
</html>

Also in the /views directory, add page2.ejs:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Second Page</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="css/main.css">
</head>

<body>
  <header>
    <h1>Second Page</h1>
    
    <% if (username) { %>
      <h2>Welcome, <%= username %>!</h2>
    <% } else { %>
      <h2>Welcome!</h2>
    <% } %>
  </header>

    <p><a href="/">Back to Main Page</a></p>
  </main>

  <footer>
    <address>&copy; 2023 Wendi Jollymore</address>
  </footer>
</body>

</html>

Let's add a controller. In the /controllers directory, add homeController.js:

exports.renderIndex = (req, res) => {
  res.render("index", {
    username: ""
  });
};

exports.getUser = (req, res) => {
  res.render("index", { 
      username: req.body.username
  });
};

// doesn't work because it's a new request/response cycle
// (this is part of the demo: why we need sessions)
exports.pageTwo = (req, res) => {
  res.render("page2", {
      username: req.body.username
  });
};

The renderIndex() function simply renders the index page with an empty username passed in (because we don't know the username, yet).

The getUser() function renders the index page also, but it a username parameter from the request body.

The pageTwo() renders page2.ejs, and also passes it a user name from the request body. However, this function isn't going to work. We'll see why in a moment.

Lastly, add the app.js file to your project.

"use strict";
const express = require("express"),
    app = express(),
    homeController = require("./controllers/homeController");

// set the port and the template engine
app.set("port", process.env.PORT || 3000);
app.set("view engine", "ejs");

// handle get requests to index
app.get("/", homeController.renderIndex);

// handle get requests to page2
app.get("/page2", homeController.pageTwo);

// all other requests probably have form data
app.use(
    express.urlencoded({ extended: false })
);

// handle post requests to getuser
app.post("/getuser", homeController.getUser);

app.listen(app.get("port"), () => {
    console.log(`Server running on http://localhost${app.get("port")}`);
});

Initialize your app, install express and ejs, and then run your application. Test it out in the browser. Your index page should load fine, but your page2 link won't work. You will see error output both in your console and in your browser, such as:

TypeError: Cannot read properties of undefined (reading 'username')
          at exports.pageTwo (C:\Users\yourname\yourprojects\sessions\controllers\homeController.js:15:28)
          at Layer.handle [as handle_request] (C:\Users\yourname\yourprojects\sessions\node_modules\express\lib\router\layer.js:95:5)
          at next (C:\Users\yourname\yourprojects\sessions\node_modules\express\lib\router\route.js:144:13)        
          at Route.dispatch (C:\Users\yourname\yourprojects\sessions\node_modules\express\lib\router\route.js:114:3)
          at Layer.handle [as handle_request] (C:\Users\yourname\yourprojects\sessions\node_modules\express\lib\router\layer.js:95:5)
          at C:\Users\yourname\yourprojects\sessions\node_modules\express\lib\router\index.js:284:15
          at Function.process_params (C:\Users\yourname\yourprojects\sessions\node_modules\express\lib\router\index.js:346:12)
          at next (C:\Users\yourname\yourprojects\sessions\node_modules\express\lib\router\index.js:280:10)        
          at expressInit (C:\Users\yourname\yourprojects\sessions\node_modules\express\lib\middleware\init.js:40:5)    
            at Layer.handle [as handle_request] (C:\Users\yourname\yourprojects\sessions\node_modules\express\lib\router\layer.js:95:5)

The key part of this error is the first line, TypeError: Cannot read properties of undefined (reading 'username'). This says that the home controller can't read the value inside req.body.username because it doesn't exist. Why doesn't req.body.username exist? We submitted the form: the .username property had a value when we viewed the output from form submission. Why is it suddenly empty?

This is because the request body and parameter data is only "alive" during a single request-response cycle: when you fill in the form and click submit, the request is sent to the server, processed and the response is sent back to the client with the "Welcome.." message on the index page. The data is available on the server for that request because it's part of this single request/response. But when you click on the page2 link, that's a new request, and the old request data is lost. The server doesn't remember you or any of the data between requests.

To fix this, we use sessions. If your app is currently running, stop running it.

First, update your app.js by adding the statement to import express session middleware:

const session = require('express-session');

Now add the express.session() middleware as the first middleware in your app:

// **** add this middleware to start a new session
app.use(session({
    secret: "thisismysecrctekey",
    saveUninitialized: false,
    resave: false 
}));

Now we need to update our controller to read/write the session data:

exports.renderIndex = (req, res) => {
  // ** updated this
  res.render("index", {session: req.session});
};

exports.getUser = (req, res) => {
  // ** updated this
  req.session.username = req.body.username;
  res.render("index", {session: req.session});
};

exports.pageTwo = (req, res) => {
  // ** updated this
  res.render("page2", {session: req.session});
};

For every handler, we added the entire session as a local variable to the various .ejs template pages. We can access this session object right inside our .ejs files: Modify the if statement and the welcome statement to use session.username instead of username:

index.ejs <header>:

<% if (session.username) { %>
  <h2>Welcome, <%= session.username %>!</h2>
<% } else { %>
  <h2>Welcome!</h2>
<% } %>

Also in index.ejs, edit the if statement above the <form> element:

<% if (!session.username) { %>

page2.ejs, edit the username in the <header> element:

<% if (session.username) { %>
  <h2>Welcome, <%= session.username %>!
<% } else { %>
  <h2>Welcome!</h2>
<% } %>

Express sessions is a separate module that needs to be installed, so before you restart your app, run npm install:
npm install express-session -s
In future, for apps that use sessions, you can install this along with Express and whatever else you want to install.

Now restart your app: you can now enter a name, and the name is available on both pages when you navigate back and forth!

Now of course, your name is always there and you can't change it. Let's add a button and some code to destroy the session so we can start over.

Add the following code to your index.ejs, right under the </form> and before the <% } %>

<% } else { %>
  <form action="/logout" method="POST">
    <div><button type="submit">Log Out</button></div>
  </form>

This adds a log out form in the Else block of your EJS if-statement: if the user is not logged in, they'll see the first form, but if they are already logged in and their name is in the session, they'll see the logout form.

Now we can add a route handler to the app.js file. Put this with the other get.post() handler:

app.post("/logout", homeController.logout);

Now we add the function to the homeController.js:

exports.logout = (req, res) => {
    req.session.destroy(() => {
        console.log("session destroyed");
    });
    res.render("index");
};

Now restart your app and try it: load the index page and enter your name, then press Submit. What happens?

You should see an error in your browser that's similar to the one below:

ReferenceError: C:\Users\yourname\yourproject\sessions\views\index.ejs:15
  13|       <h1>Index Page</h1>

  14| 

>> 15|       <% if (session.username) { %>

That's because our index.ejs is still looking for session.username, but now the session has been destroyed, so it doesn't exist.

There are several solutions to this, but an easy one also helps us create a more robust program: We should always check to make sure the session and/or session variable(s) exist before using them.

Unlike regular JavaScript, you check for the existence of variables in EJS a bit differently. In regular JavaScript, you could just use statements such as
if (session) {}
or
if (session == undefined) {}
These won't work in EJS.

In EJS, the locals property contains an object that holds all the local variables that are part of the request. So you can instead use a statement such as:

<% if (locals.session && session.username) { %>

Go ahead and update all the if statements in both the index.ejs and page2.ejs files to use locals.session to check and see if the session exists:

index.ejs and page2.ejs, if statement inside <header>:

<% if (locals.session && session.username) { %>

if statement that chooses the login form or the logout form:

<% if (!locals.session || !session.username) { %>

Now you'll find that the program works - you can submit your name, then visit page 2, then log out with no problems! You can use a similar technique to create login/logout functionality once you add data stores for your users!