Skip to content

03. Storing data in MongoDB

Nick Doiron edited this page Dec 6, 2017 · 2 revisions

Why MongoDB / NoSQL?

1batch uses MongoDB to store user sessions, user profiles, logins, and metadata/information about their photos. Specifically I'm storing it using MongoLab's free addon for Heroku.

Picking MongoDB versus other NoSQL and SQL databases can lead to endless political and technical discussions, but this example is what it is.

Session-tracking

When your user logs in, it's important that your app remembers your users' login across posts and even if you reboot your app or run it across multiple servers. Most apps handle sessions by having a set of keys in your browser cookies and in one shared database.

Install these modules (and koa-convert, if you haven't installed it yet)

npm install mongoose koa-generic-session koa-generic-session-mongo --save

In app.js, let's use MongooseJS to set up the connection to MongoDB, and then use koa-generic-session to handle the sessions.

const mongoose = require('mongoose');
const session = require('koa-generic-session');
const MongoStore = require('koa-generic-session-mongo');

// this is the initial connection by Mongoose to MongoDB
mongoose.connect(process.env.MONGOLAB_URI || process.env.MONGODB_URI || 'localhost');
...
app.keys = [process.env.SECRET_PASS_1 || '39u29fojfojf', process.env.SECRET_PASS_2 || 'feifif902i39f'];
app.use(convert(session({
  store: new MongoStore()
})));

process.env is the potential location of environment variables MONGOLAB_URI, MONGODB_URI, SECRET_PASS_1, and SECRET_PASS_2. When your source code is public, you need to store any secret passwords in environment variables. If you don't have these environment variables set, use the OR operator || so that it will look for MongoDB running on localhost, and a short string for the session secret.

You can set MONGOLAB_URI to a remote server by running this before starting your server:

export MONGOLAB_URI=http://example.com:54321
node app.js

You should also set environment variables SECRET_PASS_1 and SECRET_PASS_2 on your cloud server, so that your secret key is different from the fallback string in your public source code.

Heroku will automatically fill MONGOLAB_URI if you add a MongoLab add-on.

Making a user model with MongooseJS

MongoDB is designed to have flexible data storage, but Mongoose works with a specific data schema and uses it to validate data coming in and out of your database. Let's create a folder just for these data schemas:

mkdir models

Here's a simple data schema where each user object has a name and a password. They'll also be assigned the automatic _id field, which stores a unique MongoID of an object.

// models/user.js
const mongoose = require('mongoose');

var userSchema = mongoose.Schema({
  username: { type: String, lowercase: true },
  password: String
});

module.exports = mongoose.model('User', userSchema);

You could now save a user and return its _id like this:

// in app.js
const User = require('./models/user.js');

async function makeUser(ctx) {
  var u = new User({
    username: "x",
    password: "y"
  });
  await u.save();
  ctx.body = 'made user ' + u.username + ' with ID ' + u._id;
}

Here we use the new 'async function' and 'await' keywords to tell NodeJS to pause this function until a callback comes from the save() function.

If you've used Express and MongoDB before, you'll notice that 'await' replaces most callback functions, and any function which uses 'await' should also be marked as 'async'.

Finding users

The easiest user directory would work like this:

router.get('/users', async function (ctx) {
  // .find({}) returns all data about all users
  var users = await User.find({}).exec();
  ctx.body = users;
});

// take in an ID (using :user_id in the URL)
router.get('/users/:user_id', async function (ctx) {
  // .findById(USER_ID) returns all data about one user, if one exists with that _id
  var user = await User.findById(ctx.params.user_id).exec();
  if (!user) {
    ctx.body = 'no user with that ID';
  } else {
    ctx.body = user;
  }
});

You can use Mongoose functions to limit the data and order of data which comes back. For example, you store sensitive password information in the User object, and you don't want to show that data anywhere (even a signed-in user).

Here's a more realistic user directory:

router.get('/users', async function (ctx) {
  // find() limits which USERS come back
  // select() limits which FIELDS come back
  // sort() changes the order of the USERS
  var users = await User.find({}).select('username').sort('username').exec();
  ctx.body = users;
});

Continue to the next step to start creating some users who can sign into the app

Clone this wiki locally