A Simple Todo REST API with Node, Express and MongoDB

REST APIs and CRUD operations

REST is an acronym for REpresentational State Transfer. It is a software architecture to provide interoperability between systems on the Internet. REST-compliant Web services allow to access and manipulate textual representations of Web resources using a uniform and predefined set of stateless operations.

Every resource within a RESTful service is identified with a URI, while requests to that URI are mapped to operations on that resource. The most common architecture provides CRUD operations on resources by using the HTTP protocol, mapping each operation to a proper HTTP method.

For example, suppose a service to provide CRUD operations on a Todo list such that clients can Create, Read, Update and Delete items from that list.

A RESTful interface to such service might be the following:

Operation HTTP Verb URL
READ the whole Todo list GET /api/todos
CREATE a Todo Item POST /api/todos
READ a Todo Item GET /api/todos/:todoId
UPDATE a Todo Item PUT /api/todos/:todoId
DELETE a Todo Item DELETE /api/todos/:todoId

The Node, Express, MongoDB Stack

Node, Express and MongoDB are software stacks used to build server-side applications with JavaScript:

  • Node is an engine to run JavaScript from the command line, the same way we can run Python or BASH. It also provides an environment to build asynchronous and event driven applications. More informations can be found here.
  • Express is a software stack built over Node to ease the development of networked applications. More informations can be found here.
  • MongoDB is a non-relational database engine with dynamic schemas based on the JSON format. More informations can be found here.

When used together, Node, Express and MongoDB allow to build pure JavaScript applications using an asynchronous and event driven approach.

Application Overview

In the following we will develop a simple backend application to provide a RESTful service to manage a list of todo items with Node and Express, with storage capability provided by a MongoDB database.

The APIs exposed by the service are summarized in the following table:

Operation HTTP Verb URL
READ the whole Todo list GET /api/todos
CREATE a Todo Item POST /api/todos
READ a Todo Item GET /api/todos/:todoId
UPDATE a Todo Item PUT /api/todos/:todoId
DELETE a Todo Item DELETE /api/todos/:todoId

Prerequisites

  • The Node engine has to be properly installed and setup according to the used platform (Windows or Linux). Refer to the official documentation for how to install and setup Node.
  • The storage engine MongoDB can be either installed on the development environment or a cloud based solution can be used. I’m using a cloud based solution hosted by MongoLab.

Application Initial Setup

Application’s files will be organized with the following layout:

nodejs-todo-rest-api
    |
    +--------------- api
    |                 |
    |                 +-------- controllers // directory to hold application controllers
    |                 |
    |                 +-------- models      // directory to hold application models
    |                 |
    |                 +-------- routes      // directory to hold application routes
    |
    +---------------- server.js             // application's entry point

Supposing the existence of the previous structure, we initialize a new Node project with the command

npm init

from application’s root directory.

After answering some question about the application, a file named package.json will be created in the root directory. This file is used, among other things, to keep trace of our application’s name, starting point, and dependencies.

We then need to install Express and Mongoose packages:

  • Express, as stated above, is the stack to build networked application with Node.
  • Mongoose is a high-level stack to communicate with MongoDB databases. More informations can be found here.

We install Express with the following command (from the root directory):

npm install --save express

while to install Mongoose we can execute the command (from the root directory):

npm install --save mongoose

The Server

server.js is the entry point of the application, so its purpose is to include the packages we need, do the initial setup and open a port to listen for incoming requests:

// server.js

// call the packages we need
var express     = require('express');       // call express
var app         = express();                // define a new app with express
var bodyParser  = require('body-parser');
var mongoose    = require('mongoose');

// configure app to use bodyParser()
// this will let us get the data from a POST
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

// define application port
var port        = process.env.PORT || 3000;

// start listening for incoming connections
app.listen(port, function() {
  console.log('NodeJS TODO REST API server started on port: ' + port);
});

By executing server.js with Node:

node server.js

the following output is printed on the screen:

NodeJS TODO REST API server started on port: 3000

so the server is up and running.

The Routes

Routes are a way to map URLs to functions in our applications. Recalling the above table, we want a specific pair of (URL, HTTP Method) to map to a CRUD operation on our todo list.

We can specify this mapping using the router module of an Express application:

//api/routes/todo-routes.js

'use strict'

var express     = require('express');

// ROUTES FOR OUR API
// =============================================================================

module.exports = function(app) {

  // get an instance of the express Router
  var router = express.Router();

  //
  // configure routes
  //

  // test route, to check if server is up and running
  router.route('/')
  .get(function(req, res) {
    res.json({ message: 'NodeJS TODO REST API' });
  });

  //
  // TODO REST ROUTES
  //

  // all URLs ending with '/todos' will be routed here
  router.route('/todos')
  .get(function(req, res) {       // GET requests will execute this function
    res.json({ message: 'GET Request on /todos!' });
  })
  .post(function(req, res) {      // POST requests will execute this function
    res.json({ message: 'POST Request on /todos!' });
  })
  .delete(function(req, res) {    // DELETE requests will execute this function
    res.json({ message: 'DELETE Request on /todos!' });
  });

  // all URLs ending with '/todos/:todoId' will be routed here
  router.route('/todos/:todoId')
  .get(function(req, res) {       // GET requests will execute this function
    res.json({ message: 'GET Request on /todos/' + req.params.todoId + '!' });
  })
  .post(function(req, res) {      // POST requests will execute this function
    res.json({ message: 'POST Request on /todos/' + req.params.todoId + '!' });
  })
  .delete(function(req, res) {    // DELETE requests will execute this function
    res.json({ message: 'DELETE Request on /todos/' + req.params.todoId + '!' });
  });

  // register routes: all of our routes will be prefixed with /api
  app.use('/api', router);

}

we then register our routes by requiring the todo-routes.js module from server.js:

// server.js
...
// import the route module
var routes      = require('./api/routes/todo-routes.js');
...
// define application port
var port        = process.env.PORT || 3000;
...
// register todo routes
routes(app);
...
// start listening for incoming connections
app.listen(port, function() {
  console.log('NodeJS TODO REST API server started on port: ' + port);
});

If we run the code and start sending requests to the server, for example with POSTMan, we will get the corresponding message as the result. For example, by sending the following request:

GET 127.0.0.1/api/todos/1

we will receive:

{ message: 'GET Request on /todos/1' }

The Model

The model for the Todo item is very simple: it has a text description and a flag to state if the todo is completed or not:

// api/models/todo-model.js
'use strict';

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

var TodoSchema = new Schema({
  text: {
    type: String,
    required: 'enter the name of the task'
  },
  done: {
    type: Boolean,
    default: false
  }
});

module.exports = mongoose.model('Todo', TodoSchema);

The above code defines a new Mongoose Schema with the fields required to model our todo items. We will use an instance of this object to communicate with the MongoDB database to provide search and persistence capabilities for our todo list.

After defining the model, we need to require it from server.js, along with setting up the communication with the MongoDB database:

// server.js
...
// setup database connection
mongoose.Promise = global.Promise;
mongoose.connect(
  'connection_string_to_mongo_db_database',
  {
    useMongoClient: true
  }
);
...
// import the Todo model
var Todo        = require('./api/models/todo-model.js');
// import the route module
var routes      = require('./api/routes/todo-routes.js');
...
// define application port
var port        = process.env.PORT || 3000;
...
// register todo routes
routes(app);
...
// start listening for incoming connections
app.listen(port, function() {
  console.log('NodeJS TODO REST API server started on port: ' + port);
});

The Controller

In our architecture the controller is responsible for executing the needed actions on the Todo model to provide all functionalities exposed from the API.

For example, by sending the following request:

GET 127.0.0.1/api/todos

we are required to provide the current list of todos. We can do this defining another Node module with the proper API:

// api/controllers/todo-controller.js
'use strict';

var mongoose = require('mongoose'),

// build an instance of a Todo Schema
Todo = mongoose.model('Todo');

module.exports = {

    // function to read current todo list
    readAll: function(req, res) {

      // Use the instance of Todo Schema to retrieve the
      // current todo list from MongoDB database
      Todo.find(function(err, todos) {

        if (err) {

          res.status(400).send(err);

          return;

        }

        res.json(todos);

      });

    },

    // function to create a new Todo
    create: function(req, res) {
      // TODO
    },

    // function to get data about a Todo
    read: function(req, res) {
      // TODO
    },

    // function to updata data about a Todo
    update: function(req, res) {
      // TODO
    },

    // function to delete a Todo
    delete: function(req, res) {
      // TODO
    }

}

After defining the readAll function, we need to execute that function whenever a GET request is sent to the URL 127.0.0.1/api/todos.

We do this by requiring the controller module from the router module and by registering the function to be called by the proper handler:

// api/routes/todo-routes.js
'use strict'

var express     = require('express');

// require the controller
var todoList    = require('../controllers/todo-controller.js');

// ROUTES FOR OUR API
// =============================================================================

module.exports = function(app) {

  // get an instance of the express Router
  var router = express.Router();

  //
  // configure routes
  //

  // test route, to check if server is up and running
  router.route('/')
  .get(function(req, res) {
    res.json({ message: 'NodeJS TODO REST API' });
  });

  //
  // TODO REST ROUTES
  //

  // all URLs ending with '/todos' will be routed here
  router.route('/todos')
  .get(todoList.readAll)              // GET requests will execute this function
  .post(todoList.create)              // POST requests will execute this function

  // all URLs ending with '/todos/:todoId' will be routed here
  router.route('/todos/:todoId')
  .get(todoList.read)                 // GET requests will execute this function
  .put(todoList.update)               // PUT requests will execute this function
  .delete(todoList.delete);           // DELETE requests will execute this function

  // register routes: all of our routes will be prefixed with /api
  app.use('/api', router);

}

in the above, in addition to register the readAll handler for the route GET 127.0.0.1/api/todos, we registered handlers for all functions exported by the controller to the related route.

Function to create a new Todo

Using a software like POSTMan we can send to our API a POST 127.0.0.1/api/todos with the body cointaining the following data:

{
  "text": "Walk the Dog",
  "done": false	
}

to create a new todo item.

We can handle the request from the controller:

// api/controllers/todo-controller.js
'use strict';

var mongoose = require('mongoose'),

// build an instance of a Todo Schema
Todo = mongoose.model('Todo');

module.exports = {

    // function to read current todo list
    readAll: function(req, res) {
      ...
    },

    // function to create a new Todo
    create: function(req, res) {
      
      // create a new instance of the Todo model
      // set todo data (from the request)
      var todo = {};
      todo.text = req.body.text;
      todo.done = req.body.done;

      // save todo and check for errors
      Todo.create(todo, function(err, todo) {

        if (err) {

          res.status(400).send(err);

          return;

        }

        // echo the new created todo
        res.json(todo);

      });
      
    },

    // function to get data about a Todo
    read: function(req, res) {
      // TODO
    },

    // function to updata data about a Todo
    update: function(req, res) {
      // TODO
    },

    // function to delete a Todo
    delete: function(req, res) {
      // TODO
    }

}

Now, sending a POST 127.0.0.1/api/todos with the body cointaining the following data:

{
  "text": "Walk the Dog",
  "done": false	
}

to our API we will receive a response containing the fresh new todo item:

{
  "text": "Walk the Dog",
  "done": false,
  __v: 0,
  _id: "5a5f4fba87adcc530ee3c51c"
}

note the _id automatically added by MongoDB upon creation of the new record.

Function to get a Todo

By sending a GET 127.0.0.1/api/todos/5a5f4fba87adcc530ee3c51c we should be able to retrieve all data belonging to the todo item with id 5a5f4fba87adcc530ee3c51c.

We can handle this request from the controller:

// api/controllers/todo-controller.js
'use strict';

var mongoose = require('mongoose'),

// build an instance of a Todo Schema
Todo = mongoose.model('Todo');

module.exports = {

    // function to read current todo list
    readAll: function(req, res) {
      ...
    },

    // function to create a new Todo
    create: function(req, res) {
      ...
    },

    // function to get data about a Todo
    read: function(req, res) {
      
      // use the findById() function from Todo Schema to retrieve
      // a todo with the specified id
      Todo.findById(req.params.todoId, function(err, todo) {

        if (err) {

          res.status(400).send(err);

          return;

        }

        res.json(todo);

      });
      
    },

    // function to updata data about a Todo
    update: function(req, res) {
      // TODO
    },

    // function to delete a Todo
    delete: function(req, res) {
      // TODO
    }

}

Function to update a Todo

By sending a PUT 127.0.0.1/api/todos/5a5f4fba87adcc530ee3c51c request with the following body:

{
  _id: "5a5f4fba87adcc530ee3c51c",
  "text": "Walk the Dog",
  "done": true
}

we should be able to update specified data belonging to the todo item with id 5a5f4fba87adcc530ee3c51c.

We can handle this request from the controller:

// api/controllers/todo-controller.js
'use strict';

var mongoose = require('mongoose'),

// build an instance of a Todo Schema
Todo = mongoose.model('Todo');

module.exports = {

    // function to read current todo list
    readAll: function(req, res) {
      ...
    },

    // function to create a new Todo
    create: function(req, res) {
      ...
    },

    // function to get data about a Todo
    read: function(req, res) {
      ...
    },

    // function to updata data about a Todo
    update: function(req, res) {
      
      // use the findById() function from Todo Schema to retrieve
      // a todo with the specified id
      Todo.findById(req.params.todoId, function(err, todo) {

        if (err) {

          res.status(400).send(err);

          return;

        }

	// update todo informations according to what
	// is specified in the body of the request
        todo.text = req.body.text;
        todo.done = req.body.done;

	// save the updated todo item
        todo.save(function(err) {

          if (err) {

            res.status(400).send(err);

            return;

          }

          res.json({ message: 'Todo Updated!' });

        });

      });
      
    },

    // function to delete a Todo
    delete: function(req, res) {
      // TODO
    }

}

Function to delete a Todo

By sending a DELETE 127.0.0.1/api/todos/5a5f4fba87adcc530ee3c51c we should be able to delete the todo item with id 5a5f4fba87adcc530ee3c51c.

We can handle this request from the controller:

// api/controllers/todo-controller.js
'use strict';

var mongoose = require('mongoose'),

// build an instance of a Todo Schema
Todo = mongoose.model('Todo');

module.exports = {

    // function to read current todo list
    readAll: function(req, res) {
      ...
    },

    // function to create a new Todo
    create: function(req, res) {
      ...
    },

    // function to get data about a Todo
    read: function(req, res) {
      ...
    },

    // function to updata data about a Todo
    update: function(req, res) {
      ...
    },

    // function to delete a Todo
    delete: function(req, res) {
      
      // use the remove() function from Todo Schema to delete
      // todos that match the specified conditions
      Todo.remove(
	  {
            _id: req.params.todoId
	  },
	  function(err) {

	      if (err) {

	        res.status(400).send(err);

	        return;
	      }

	      res.json({ message: 'Todo Deleted!' });
	  }

	  );
      
     }

}

The Full Code

The full code for the Simple Todo REST API with Node, Express and MongoDB is available here.

Written on January 18, 2018