Roll your own session storage for Node.js / Express
If you're using Node.js with Express to build generic web projects, you'll have to address, sooner or later, the issue of session data storage.
Keeping the sessions in memory is obviously a bad idea for a number of reasons (memory leaks, clustering / concurrency issues, etc.).
Also, there's another reason why you should pay special attention to this subject: assuming your website is public and has thousands of pages / URLs, the Google / Bing indexing bots will most likely make lots of requests trying to figure out what's in there, especially after submitting your sitemap.xml (note: generating a sitemap.xml using Jade template engine is quite easy).
Unfortunately the indexing bots won't keep cookies, so each of those tens of thousands requests will most likely trigger its own session, so your session storage might end up quite large if you're not careful.
While there are, of course, a number of npm modules out there (like this one), if you want full control over what's happening in there, or if you're just looking to keep dependencies down to a minimum, rolling out your own solution based on Connect-middleware proves to be a fairly easy task.
Here's how you could do it, assuming you're already using Mongoose to persist your data in MongoDB (>2.2 for TTL collections feature):
models/session.coffee
:
mongoose = require 'mongoose'
schema = new mongoose.Schema
_id: String
data:
type: mongoose.Schema.Types.Mixed
required: yes
usedAt:
type: Date
expires: process.env.SESSION_TTL * 3600
,
versionKey: no
model = mongoose.model 'Session', schema
module.exports = model
helpers/session-store.coffee
:
Session = require '../models/session'
noop = ->
module.exports = (connect) ->
class SessionStore extends connect.session.Store
get: (_id, cb) ->
Session.findById(_id).lean().exec (err, session) ->
return cb err if err
cb null, session?.data
set: (_id, data, cb = noop) ->
return Session.remove { _id }, cb unless data
Session.update { _id }, { data, usedAt: new Date }, { upsert: yes }, cb
destroy: (_id, cb = noop) ->
Session.remove { _id }, cb
app.coffee
:
express = require 'express'
http = require 'http'
mongoose = require 'mongoose'
SessionStore = require('./helpers/session-store') express
# other imports, etc...
env = process.env
mongoose.connect env.MONGODB_URI, { db: { native_parser: yes } }
app = express()
# various middleware
app.use express.session
secret: env.SESSION_KEY
cookie:
maxAge: env.SESSION_TTL * 3600000
store: new SessionStore
# Set-up routes...
# Start listening
http.createServer(app).listen app.get('port'), ->
console.log "Express server listening on port #{app.get('port')}"
And don't forget to set SESSION_TTL environment variable to the number of hours you want to keep the sessions alive for...
Written by Ionut-Cristian Florescu
Related protips
6 Responses
Another awesome tip! Thanks for sharing!
@jonahoffline - Thanks for reading, Jonah!
I was using connect-mongo and Mongoose with MongoDB 2.4.5 in one of my projects, and I was a bit annoyed by the huge dependency-tree. Plus, they were using different versions of bson
native parser, so I thought this should tidy up the project a bit...
I use connect-mongo and I have one problem - I want user session to be removed/invalidated if the user logs in on another device. Unfortunately this doesn't seem to happen. Do you think I can overcome this by rolling my own session?
@pavelnikolov - you'll definitely have to implement your own logic for that... It's probably fairly easy to do if you're using passport.
Great article, thanks. Found one oddity in the implementation here; I was ending up with cookie objects in Mongo that had a 'data' subobject, containing unmerged fields. To resolve this, I run the inbound data on the set method through this function:
serialize = (session) ->
data = {}
for key, value of session
data[key] = value?.toJSON?() or value unless typeof value is 'function'
data
@bazineta - I'm just curious, why would you want to merge those fields?