0mvhpw
Last Updated: May 30, 2016
·
12.64K
· icflorescu
Icflorescu

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.

Session storage in Node.js / Express

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...

6 Responses
Add your response

10592
4693d7cfa88635d430c0de9a92f8dd84

Another awesome tip! Thanks for sharing!

over 1 year ago ·
10593
Icflorescu

@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...

over 1 year ago ·
12234
Me normal

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?

over 1 year ago ·
12276
Icflorescu

@pavelnikolov - you'll definitely have to implement your own logic for that... It's probably fairly easy to do if you're using passport.

over 1 year ago ·
12314
F201059f3f8d1c4a909151bf2c39c3f2

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
over 1 year ago ·
12392
Icflorescu

@bazineta - I'm just curious, why would you want to merge those fields?

over 1 year ago ·