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.
secret
: a random unique string used to
authenticate the session. typically this would be randomly generated
but for demonstrations/debugging just put any string you like.
resave
: a boolean (default: true) that configures
whether or not the session data is saved on this request when
the session has not been modified. Usually you would set this
to false
: there's no point in wasting resources saving
a session that hasn't been modified.
saveUnitialized
: a boolean (default: true) that
configures whether or not an uninitialized session should be saved.
An uninitialized session is a session that has just been created
but not modified (had no data saved to it yet).
Setting this property to true allows an uninitialized session to
be saved, and setting to false will not save if not the session is
uninitialized. Setting this to false will improves performance;
setting it to true can cause problems in some programs if you
don't know what you're doing, and can violate certain country
laws that require permission before setting any kind of cookie
on the user's computer.
cookie
: an object with its own properties to
configure the session cookie. These properties include:
- maxAge: sets the cookie expiry time in milliseconds
- expires: accetpts a date object for expiry
- There are other properties you can learn about 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>© 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>© 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!