#100DaysOfMERN - Day 29

#100DaysOfMERN - Day 29

·

14 min read

✏ Mongoose

Continuing with MongoDB (used as local database), I'll now look into Mongoose. It's an npm package that is apparently of great assistance when communicating with your database (it also comes with excellent documentation).

Installing it:

npm install mongoose

The first file

The first thing that Mongoose will have to do is connect to the database, so I'll create the first file connection.js and get Mongoose in:

const mongoose = require('mongoose');

If you console.log what you just imported (I usually do that, not super helpful at all times, but you get a rough idea what this thing is supposed to do (and can do) from the property names) - you'll find stuff like "connections", "models", "Schemas", "modelSchemas", "Types", and "options". I'm going to learn about all that soon.

Establishing a connection

The syntax is the same as before:

const mongoose = require('mongoose');

const connStr = 'mongodb://localhost/merndb';

const connOptions = {
  useNewUrlParser: true,
  useUnifiedTopology: true
};

mongoose.connect(connStr, connOptions);

Adding a little script in the package.json:

"scripts": {
    "conn": "node connection.js"
  }

Let's see what happens:

npm run conn

Well, I don't get an error (which is good), but I can't really do anything with my connection yet. I don't even know if it was successful.

The .connect method

This method returns a promise, which would like to be consumed appropriately, so I'll use a .then chain:

mongoose.connect(connStr, connOptions)
    .then(() => console.log('connected'))
    .catch(err => console.log(`connection error: ${err}`))

Although there's nothing wrong with this code, I ran into several issues with this. A first test seemed to be successful, and the console logged:

connected

Yay. But of course you also try what happens if something goes wrong.

Wrong host name

I changed the connection string to mongodb://localhostingservice/merndb - and weirdly nothing got logged, so obviously the promise was still pending. I then tried a lot because I thought my code was wrong, but it turned out that sometimes a solution suddenly appears if you just cluelessly stare at the screen for a while.

A wrong hostname will cause an error, but Mongoose allows some time for the connection to establish, which makes sense - it won't fail right away, but will keep trying for a certain amount of time. After 30 seconds, my error message suddenly popped up in the console.

This is configurable though, by adding another property to the options object:

const connOptions = {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  serverSelectionTimeoutMS: 5000
};

Since I'm working with a local database, 5 seconds should be enough to establish a connection.

Wrong database name

What happens if I try to connect to the correct host, but use the wrong database name like mongodb://localhost/merndbbbbb? Without delay, the log says:

connected

The reason for this is: if you try to connect to a database that doesn't exist, it won't throw an error, it'll instead create a new database with that name.

There's no worries though that you'll be creating databases left and right each time you have a typo somewhere, because the database will only be saved if you also create a collection within.

I probably would get an error if I'm trying to perform operations on that empty database later on in the code.


✏ Models and Schemas

Mongoose works with Models and Schemas. MongoDB itself has no restrictions concerning the structure of your data, it can be completely unstructured if you like, but when working with Mongoose, the data of a collection has to follow a certain pattern, as defined in a Schema.

Each new record will be created based on that Schema. To start, I'll create a models folder and within, a postmodel.js file, where I bring in Mongoose and get its Schema method, which is a constructor function for creating new Schema objects:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

I've already thought about the structure of my collection of blog posts on Day 27, and this was what I came up with:

{
    title: '#100 Days of MERN - Day 1',
    tags: ['mern', 'node', 'command line'],
    summary: ['What is the MERN stack', 'How to install Node.js'],
    published:new Date(2020, 11, 29).toLocaleDateString("en-GB"),
    slug: '/100daysofmern-day-1',
    date: Date()
}

Creating a Schema

Translating that into a Schema is rather trivial now (for a more advanced way to create a Schema, see below):

const PostSchema = new Schema({
    title: String,
    tags: [ String ],
    summary: [ String ],
    published: String,
    slug: String,
    date: Date
})

Creating a Model

The Schema is the blueprint that the Model is based on, and a collection consists of a number of instances of this Model. Mongoose offers a .model method, which takes the name of the Model, the defined Schema and the collection name as parameters. This model will then serve as an interface, through which I can query the database:

const PostModel = mongoose.model('post', PostSchema, 'posts');

(Note: The above is slightly redundant, I could've omitted the name of the collection, because then, Mongo will pluralise the model name (post) and take that as collection name)

Now, all I have to do is export it, so I can use it elsewhere in my code, where I'm actually filling/querying/manipulating my database:

module.exports = PostModel

✏ Saving to the Database

I'll throw this code in my connection.js file for now, as a first test. So far, it's only connecting to Mongo, but now I'd also like to create a new Post record, based on the (imported) Post Model, and save it to the database. Mongoose comes with a handy .save method, which will abstract away what I earlier did manually, using .insertOne. So I'll now

  • create a new instance of my Post Model
  • use its .save method to save it to the collection that is based on this Model

connection.js

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

const connStr = 'mongodb://localhost/merndb';

const connOptions = {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  serverSelectionTimeoutMS: 5000
};

mongoose.connect(connStr, connOptions)
    .then(() => console.log('connected'))
    .catch(err => console.log(`connection error: ${err}`))


// create a new post
const newPost = new PostModel({
    title:'Day 29',
    tags:['test'],
    summary:['test'],
    published:'2021-02-15',
    date:Date()
});

// save it
newPost.save(function(err){
    if (err) return console.log('error during save: ', err)
})

Note that when creating a new record, I don't have to fill out all the properties defined in the Schema. I could for example create a new post with only a title - then the record in the database would still have all the other fields, they'd just be empty.

However, sometimes it makes sense to first make sure that a record is only written to the database if all fields contain some data. This is done with a slight modification of the Schema:


✏ Creating a more advanced Schema

In my above Schema definition, I've only added some properties that I'd like my record documents to have, and also defined their type:

const PostSchema = new Schema({
    title: String,
    tags: [ String ],
    ...
})

Instead, I could put an object there with some options, like setting the fields to required, or make sure they're unique.

The Schema constructor also takes a second object to set some options. It's not necessary to add a field for the creationdate for each post, for example, I can instead let Mongoose do that for me with the timestamps option. This will add two more properties of type Date to each post: createdAt and updatedAt.

const PostSchema = new Schema({
    title:{
      type: String,
      required: true,
      unique: true
    },
    tags:[ { type: String, required: true } ],
    ...
}, { timestamps: true })

While all this works as intended - I can see either by opening Compass or by checking in the Mongo Shell that my new post was indeed saved to the database - I tried to break the code at different spots again, to see what happens. Turns out that stuff often fails silently, and I get no notification of an error, which is something that should be addressed in a future post.

✏ Recap

I've learned

  • how to use Mongoose - a comfy layer between my code and MongoDB
  • how to establish and configure the connection to MongoDB
  • how to define a Schema
  • how to create a Model from this Schema
  • how to write to the database using thesave method

✏ Next:

  • the backend for my #100DaysOfMERN app

✏ Thanks for reading!

I do my best to thoroughly research the things I learn, but if you find any errors or have additions, please leave a comment below, or @ me on Twitter. If you liked this post, I invite you to subsribe to my newsletter. Until next time 👋


✏ Previous Posts

  • Day 1: Introduction, Node.js, Node.js in the terminal
  • Day 2: npm, node_modules, package.json and package-lock.json, local vs global installation of packages
  • Day 3: Create a React app without create-react-app, Webpack, Babel
  • Day 4: npx and cowsay
  • Day 5: npm vs. npx, npm audit, semantic versioning and update rules
  • Day 6: Call stack, event loop, JavaScript engine, JavaScript runtime
  • Day 7: Call stack and event loop in Node.js, setImmediate()
  • Day 8: setImmediate(), process.nextTick(), event loop phases in Node.js
  • Day 9: Network requests with XMLHttpRequest and callbacks
  • Day 10: Promises
  • Day 11: Network requests with XMLHttpRequest and Promises
  • Day 12: React Quiz App part 1
  • Day 13: React Hangman
  • Day 14: FullStackOpen course 1: details of GET and POST requests, request headers
  • Day 15: React Hangman: Trigger fetch with click event callback vs useEffect
  • Day 16: REST API and CRUD
  • Day 17: Boring Book App part 1: React Frontend, Express Backend, GET requests, CORS
  • Day 18: Boring Book App part 2: POST request, File System API
  • Day 19: Boring Book App part 3: Request Parameters, DELETE request
  • Day 20: Boring Book App part 4: PUT request
  • Day 21: Express JS vs Vanilla JS part 1: Server setup, routes
  • Day 22: Express JS vs Vanilla JS part 2: Serve static files with Vanilla Server
  • Day 23: Express JS vs Vanilla JS part 3: Serve static files with Express Server, Middleware
  • Day 24: Express JS: express.Router, Postman
  • Day 25: Express JS: express-handlebars
  • Day 26: MongoDB: Installation, noSQL database structure, Mongo Shell commands
  • Day 27: MongoDB: Project setup, JSON vs BSON, connecting to database, inserting a record
  • Day 28: MongoDB: connect from a server and read/modify data