Persisting Express session in DynamoDB container
DynamoDB is a highly available NoSQL database, which supports key/value and document type data. I wrote more about DynamoDB in this post, so I won’t cover these concepts here. If you are not familiar with the basics of DynamoDB, please read that post first.
1. Pre-requisites
The following tools have to be installed on your machine (other than Node.js) if you want to recreate this mini-project:
- Docker
- AWS CLI
- and have a free AWS account to get the access key and secret access key.
You’ll also need to configure the AWS CLI for the live access, which sets up the credentials in a file called credentials
, and places it in the .aws
folder.
2. Add DynamoDB locally
Luckily, it’s possible to set up DynamoDB locally through a Docker image, so we don’t have to connect to AWS and use their service for testing our code. Everything we’ll do here can be reproduced on AWS by replacing the local endpoint
in the configuration object with the real one.
In order to set up the database container, we can create a super simple docker-compose.yml
file in the root folder:
version: '3'
services:
db:
image: amazon/dynamodb-local
ports:
- '8000:8000'
The name of the image we run the container off of is called amazon/dynamodb-local
, and it will listen to port 8000
.
3. Install the packages
Let’s start by creating a project folder:
mkdir session-with-dynamodb && cd session-with-dynamodb
From the project folder, type npm init -y
. This command will create the package.json
file, and it’ll also skip the questions npm
asks when the folder is being set up.
Next, we’ll install the packages:
npm i express express-session aws-sdk connect-dynamodb
Let’s briefly talk about these dependencies.
Express is the most popular web framework for Node.js. It’s widely used, and has a rich interface for creating web servers and API endpoints.
The session data will be provided by express-session, and it’ll be incorporated into the code as middleware.
AWS SDK provides a Node -wrapper around the AWS services.
Finally, Connect DynamoDB provides the DynamoDB session store.
4. Add the code
Now that the dependencies are installed, let’s create the server.js
file in the root of the project folder. The file can look like this:
const express = require('express')
const session = require('express-session')
const AWS = require('aws-sdk')
const DynamoDBStore = require('connect-dynamodb')(session)
const app = express()
AWS.config.update({
region: 'us-west-2',
endpoint: 'http://localhost:8000'
})
const dynamodb = new AWS.DynamoDB()
const PORT = 3000
app.use(session({
secret: 'my-secret',
resave: false,
saveUninitialized: false,
store: new DynamoDBStore({
client: dynamodb,
table: 'session'
})
}))
app.use((req, res, next) => {
if (!req.session.views) {
req.session.views = 0
}
next()
})
app.get('/private', (req, res) => {
const numberOfViews = ++req.session.views
res.send(`The /private page has been visited ${ numberOfViews } times.`)
})
app.listen(PORT, () => {
console.log(`Server is running on port ${ PORT }`)
})
Here’s the step-by-step explanation of the code.
4.1. Session
We start by require
-ing the dependencies at the top of the file, and then we initiate the app
by calling the express
function.
Next, we update the AWS configuration. The compulsory properties are the region
(it won’t be used, because we’ll do everything locally, but it’s necessary for the live connection) and the endpoint
, which is localhost:8000
(this needs to be replaced with the real DynamoDB endpoint in production). This is the port on our machine that the DynamoDB container will open up for us.
After initiating DynamoDB and declaring the PORT
, we’ll create the session middleware using app.use
.
express-session
has a bunch of properties, and only a fraction of them is used here.
The secret
is usually stored as an environment variable, here we’ll be fine with some utterly secure secret. resave
and saveUninitialized
are set to false
, they both help preventing race conditions from happening when the client makes parallel requests.
The store
property is very important: This is where we can define the database that stores the session. We define the dynamodb
instance as the client
(i.e. the database which stores the session data), and give a name to the table (session
) for the session. The default table name is sessions
, but I thought it would be cool to change it, and demonstrate the usefulness of the table
property.
With this, we have set up the session store. There’s no need to manually create the table, because connect-dynamodb
will do this for us.
4.2. Endpoint and server
We can create another middleware with app.use
, and this will be a simple counter, which counts the number times our endpoint is hit. This is done by creating a property called views
in the session
object (which will be added by express-session
), and set its initial value to 0
.
It’s important to call next
inside the middleware. If we miss this step, the code flow will stop here, and the server will simply hang. When next
is called, the control is passed on to the next line in the file.
This very next line is the only endpoint we define here (/private
). It could be anything else, feel free to play around with it and add more endpoints. Each time the /private
endpoint is hit, we’ll increase the number of views by one, and the current number of visits will be returned in the res.send
method.
Finally, we start the server by calling the listen
method.
5. Run the code and test it
We can now start the server and do some tests!
5.1. Start the database and the server
Let’s run the DynamoDB container in the terminal, and start the server in another terminal:
docker-compose up
and
node server.js
When the server is started, connect-dynamodb
will automatically create the session
table using the create-table
command behind the scenes. We’ll receive some pieces of really useful information about the session
table, for example, the time and date of creation or the status of the table (ACTIVE
). A very important key is the ItemCount
, which is initially equal to 0
. This comes as no surprise as we haven’t created any items (i.e. no sessions have been initiated) yet.
5.2. Test the code and use AWS CLI to query the table
We can use the AWS CLI to make some queries. First, let’s list all tables (it’s not hard to count them, there should only be one). In another terminal, run the following command:
aws dynamodb list-tables --endpoint http://localhost:8000
This will return all tables for this endpoint (which points to the DynamoDB Docker container). The list is not particularly long:
{
"TableNames": [
"session"
]
}
So let’s open the browser, and type localhost:3000/private
in the address bar. The message on the screen should be The /private page has been visited 1 times.
.
This means that the counter started to work, and the session data should already be saved to the DynamoDB session
table.
We can quickly check this by running the following command from the terminal:
aws dynamodb describe-table --table-name session --endpoint http://localhost:8000
The describe-table
command has another compulsory option called table-name
, and we have to name the table we want to learn more about. In our case, this table is session
.
The return value should be similar to this:
{
"Table": {
"AttributeDefinitions": [
{
"AttributeName": "id",
"AttributeType": "S"
}
],
"TableName": "session",
"KeySchema": [
{
"AttributeName": "id",
"KeyType": "HASH"
}
],
"TableStatus": "ACTIVE",
"CreationDateTime": 1557746582.467,
"ProvisionedThroughput": {
"LastIncreaseDateTime": 0.0,
"LastDecreaseDateTime": 0.0,
"NumberOfDecreasesToday": 0,
"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5
},
"TableSizeBytes": 161,
"ItemCount": 1,
"TableArn": "arn:aws:dynamodb:ddblocal:000000000000:table/session",
"BillingModeSummary": {
"BillingMode": "PROVISIONED",
"LastUpdateToPayPerRequestDateTime": 0.0
}
}
}
These details are very useful, and many of them are covered in the DynamoDB post I referred to earlier. But the most relevant part of the data is the ItemCount
, which shows 1
instead of 0
, that is the session data have been successfully stored.
If we refresh the browser, we’ll see that the counter increments the number of visits.
Let’s quickly open the developer tools in the browser, and go to Application
, and Cookies
. express-session
placed a cookie called connect.sid
in the browser, so the session will be kept if the browser is refreshed.
On the server side the cookie
is stored in the req.session
object.
5.3. Test if the session is persisted - it should be
Let’s stop the server now, and restart it, then refresh the browser again. With this, we can simulate a server crash.
If the session wasn’t persisted, the counter would be reset to 1
, and a new session id would be placed in the cookie. But we don’t see this; instead, the number of visits has been kept, and has incremented again.
Persisting the session is useful in case of a server crash. The user won’t have to restart everything by logging in again, which is great!
It’s also possible to set an expiration in express-session
, which we are not doing now. If you are interested, please read the documentation about the available options.
Let’s run the AWS CLI command again:
aws dynamodb describe-table --table-name session --endpoint http://localhost:8000
The value of ItemCount
is still 1
, because the same user with the same browser is connected to the server (although the server has been crashed/restarted).
5.4. Add a second session
Now from a different browser (or using curl
), let’s connect to locahost:3000/private
again, and simulate a new user connecting to the server.
The return message should be The /private page has been visited 1 times.
again, because it’s a new connection, and that user hasn’t visited the page yet.
Run the AWS CLI command again. Guess what will be the value of ItemCount
:
aws dynamodb describe-table --table-name session --endpoint http://localhost:8000
That’s right, it will be 2
, because a new session has been initiated.
And that’s it, this is how we can store and persist session data in DynamoDB. Now we can stop the container (docker-compose down
) and the server (Ctrl + C
).
6. Conclusion
One of the use cases of AWS DynamoDB is to store web session data as DynamoDB is fast and highly available. It’s possible to test DynamoDB locally by using a Docker image and running a container off of it.
The express-session
library is responsible for establishing the session, and placing the session id in the cookie. But, by default, it’s an in-memory store. If we want to persist it, we need to use one of the connect
modules - in this case, it’s connect-dynamodb
.
Thanks for reading, and see you next time.