DEV Community

Alberto Fernandez Medina
Alberto Fernandez Medina

Posted on • Updated on • Originally published at onlythepixel.com

Mongoose, MongoDB and Express

First steps on ExpressJS API development to connect to a MongoDB database and start managing data using endpoints.

Note: This is the 3rd post of a series of post about Building APIs With Express. Based on my last post about API Routing with Express I'll continue developing over the generated code.

Last time, the awesome TODO API was leaved with a nice API Routing hierarchy, but! And this is an important "but". I didn't store any kind of data for future use, it's only storing the TODOs in temporal memory so once the server stops all the info is lost.

This post was first published on onlythepixel.com

Requirements

For this posts, I'll need to have installed on my machine MongoDB to being able to develop my API with real connections in my local environment.

Note: I need to pay attention to have my MongoDB up and running to being able to work with it.

Also, I'm going to need Mongoose as a dependency of my project, this package will help me with the DB communication and data models:

yarn add mongoose
Enter fullscreen mode Exit fullscreen mode

Mongoose connection to MongoDB

First I need to let mongoose connect to my local MongoDB so I'm going to create a new script to take this job.

src/db.js

const mongoose = require('mongoose')
const debug = require('debug')
const log = debug('another-todo:database')
const error = debug('another-todo:database:error')

// First I define my DB URI or
// make my script take it from the env variables
const DB_URI = process.env.DB_URI || 'mongodb://localhost/another-todo'

// Define some basic methods to
// connect/disconnect to the DB
const db = {

  connect () {
    return mongoose.connect(DB_URI)
  },

  disconnect () {
    return mongoose.connection.close(() => {
      process.exit(0)
    })
  }
}

// This let mongoose use the node's default promises
mongoose.Promise = global.Promise

// Logs for our app
mongoose.connection.on('connected', () => {
  log('Mongoose connection open to ' + DB_URI)
})

// More logs...
mongoose.connection.on('disconnected', () => {
  log('Mongoose disconnected')
})

// Logs that I hope to not see
mongoose.connection.on('error', (err) => {
  error(err)
})

// Handle process terminations
// this ensures that there is any connection
// open with DB when I stop the app
process
  .on('SIGINT', db.disconnect)
  .on('SIGTERM', db.disconnect)

// finally I only expose the methods to being used by my app script
module.exports = db

Enter fullscreen mode Exit fullscreen mode

Now I only need to use my db script on my app.

src/index.js

const express = require('express')
const logger = require('morgan')
const bodyParser = require('body-parser')
const app = express()
const v1 = require('./v1')
const db = require('./db')

// Connect to DB!!
db.connect()

// Middlewares
...
Enter fullscreen mode Exit fullscreen mode

Mongoose Models

Now it's time to define the first mongoose model, at this moment the only model or relevant data to store in DB are my tasks so so I only going to need the model.

I'm going to use the same data structure that I used in my last post.

src/models/task.js

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

// I'm going to define a new schema
// Here is where I define the properties
// that my data is going to have
// along with its validations 
const taskSchema = new Schema({

  // A property 'description' of type string
  // with a default to a empty string
  description: {
    type: String,
    default: ''
  },

  // And a boolean property with false as default
  isDone: {
    type: Boolean,
    default: false,
    required: true
  }

}, {timestamps: true})

module.exports = mongoose.model('Task', taskSchema)

Enter fullscreen mode Exit fullscreen mode

Note: that timestamps let me not having to define a property createdAt or updatedAt because it is going to add this value once that property is set to true.

Now is time to make use of this model in my API.

src\v1\tasks.js

const router = require('express').Router()
const Task = require('../models/task')

router.route('/')

  .get((req, res, next) => {
    // I exec the find without conditions 
    // to retrieve all my tasks
    Task.find((err, tasks) => {
      if (err) return next(err)

      return res.json(tasks)
    })
  })

  .post((req, res, next) => {
    Task.create(req.body, (err, task) => {
      if (err) return next(err)

      return res.status(201).json(task)
    })
  })

  .delete((req, res, next) => {
    // This method is similar to find but instead
    // it removes all the occurrences 
    Task.remove((err) => {
      if (err) return next(err)

      return res.status(204).end()
    })

    res.status(204).end()
  })

router.param('taskId', (req, res, next, id) => {
  // Handle to find the requested resouce
  Task.findById(id, (err, task) => {
    if (err) return next(err)

    // If the task is not found then the app returns a 404
    if (!task) {
      err = new Error('Task not found')
      err.status = 404
    } else {
      req.task = task
    }

    return next(err)
  })
})

router.route('/:taskId')

  .get((req, res, next) => {
    return res.json(req.task)
  })

  .put((req, res, next) => {
    // I'm not using req.task.update() because
    // that method doesn't return the task on the callback
    Task.findByIdAndUpdate(req.task.id, {
      $set: req.body
    }, {
      // Returns the updated task
      new: true,
      // Set the whole document even if we are not
      // receiving all the properties
      overwrite: true,
      // Run validations if we have them
      runValidators: true
    }, (err, task) => {
      if (err) return next(err)

      return res.json(task)
    })
  })

  .patch((req, res, next) => {
    Task.findByIdAndUpdate(req.task.id, {
      $set: req.body
    }, {
      new: true,
      runValidators: true
    }, (err, task) => {
      if (err) return next(err)

      return res.json(task)
    })
  })

  .delete((req, res, next) => {
    Task.findByIdAndRemove(req.task.id, (err) => {
      if (err) return next(err)

      res.status(204).end()
    })
  })

module.exports = router

Enter fullscreen mode Exit fullscreen mode

Note: You can check the Mongoose API docs for info about its different methods.

Now it's time to try it!

cURL

$ curl -X GET "http://localhost:3000/v1/tasks"

[]

$ curl -X POST "http://localhost:3000/v1/tasks" \
> -H "Content-Type: application/x-www-form-urlencoded" \
> -d 'description=test'

{
    "__v": 0,
    "updatedAt": "2017-01-05T17:53:37.066Z",
    "createdAt": "2017-01-05T17:53:37.066Z",
    "_id": "586e88217106b038d820a54e",
    "isDone": false,
    "description": "test"
}

$ curl -X POST "http://localhost:3000/v1/tasks" \
> -H "Content-Type: application/x-www-form-urlencoded" \
> -d 'description=test'

{
    "__v": 0,
    "updatedAt": "2017-01-05T17:53:55.067Z",
    "createdAt": "2017-01-05T17:53:55.067Z",
    "_id": "586e88337106b038d820a54f",
    "isDone": false,
    "description": "test"
}
$ curl -X GET "http://localhost:3000/v1/tasks"

[
    {
        "__v": 0,
        "updatedAt": "2017-01-05T17:53:37.066Z",
        "createdAt": "2017-01-05T17:53:37.066Z",
        "_id": "586e88217106b038d820a54e",
        "isDone": false,
        "description": "test"
    },
    {
        "__v": 0,
        "updatedAt": "2017-01-05T17:53:55.067Z",
        "createdAt": "2017-01-05T17:53:55.067Z",
        "_id": "586e88337106b038d820a54f",
        "isDone": false,
        "description": "test"
    }
]

$ curl -X DELETE -i "http://localhost:3000/v1/tasks"

HTTP/1.1 204 No Content
X-Powered-By: Express
Date: Thu, 05 Jan 2017 17:54:47 GMT
Connection: keep-alive

$ curl -X POST "http://localhost:3000/v1/tasks" \
> -H "Content-Type: application/x-www-form-urlencoded" \
> -d 'description=test'

{
    "__v": 0,
    "updatedAt": "2017-01-05T17:54:53.555Z",
    "createdAt": "2017-01-05T17:54:53.555Z",
    "_id": "586e886d7106b038d820a550",
    "isDone": false,
    "description": "test"
}

$ curl -X GET "http://localhost:3000/v1/tasks/586e886d7106b038d820a550"

{
    "_id": "586e886d7106b038d820a550",
    "updatedAt": "2017-01-05T17:54:53.555Z",
    "createdAt": "2017-01-05T17:54:53.555Z",
    "__v": 0,
    "isDone": false,
    "description": "test"
}

$ curl -X PATCH "http://localhost:3000/v1/tasks/586e886d7106b038d820a550" \
> -H "Content-Type: application/x-www-form-urlencoded" \
> -d 'description=amazing' 

{
    "_id": "586e886d7106b038d820a550",
    "updatedAt": "2017-01-05T17:56:06.879Z",
    "createdAt": "2017-01-05T17:54:53.555Z",
    "__v": 0,
    "isDone": false,
    "description": "amazing"
}

$ curl -X PATCH "http://localhost:3000/v1/tasks/586e886d7106b038d820a550" \
> -H "Content-Type: application/x-www-form-urlencoded" \
> -d 'isDone=true' 

{
    "_id": "586e886d7106b038d820a550",
    "updatedAt": "2017-01-05T17:56:24.328Z",
    "createdAt": "2017-01-05T17:54:53.555Z",
    "__v": 0,
    "isDone": true,
    "description": "amazing"
}

$ curl -X PUT "http://localhost:3000/v1/tasks/586e886d7106b038d820a550" \
> -H "Content-Type: application/x-www-form-urlencoded" \
> -d 'isDone=false' 

{
    "_id": "586e886d7106b038d820a550",
    "createdAt": "2017-01-05T17:56:40.478Z",
    "updatedAt": "2017-01-05T17:56:40.478Z",
    "isDone": false,
    "description": ""
}

$ curl -X DELETE -i "http://localhost:3000/v1/tasks/586e886d7106b038d820a550"

HTTP/1.1 204 No Content
X-Powered-By: Express
Date: Thu, 05 Jan 2017 17:57:35 GMT
Connection: keep-alive

$ curl -X GET "http://localhost:3000/v1/tasks"

[]
Enter fullscreen mode Exit fullscreen mode

If the server stops and starts again the tasks still there, mission accomplished!

Note: I recommend to use Postman to test the API instead of cURL.

That all I think. You can check the code on GitHub.

Top comments (2)

Collapse
 
mrm8488 profile image
Manuel Romero

Another great article, Alberto. I like the way you structure the code. It is so clean and readable.

Collapse
 
albertofdzm profile image
Alberto Fernandez Medina

Glad to read it! Thanks for the comment, I really appreciate to know that is being helpful to someone