Bootcamp Notes – Day 8 (Fri) – NodeJS: Week 2: REST API with Express, MongoDB, and Mongoose

 

REST API with Express, MongoDB, and Mongoose

 

Overview

Last week we learned about building a REST APi server with Express in Node that is able to respond to HTTP requests to GET, POST, PUT, and DELETE for various endpoint paths. But we built it just to demonstrate that we could access the those endpoints and get a response back from the Express server. At that point, we did not have a database set up and we didn’t know how to communicate with the database to actually GET, PUT, POST and DELETE any data. That is part of the puzzle that we have accomplished this week.

Things we learned recently:

  • About the fundamental concepts of MongoDB
  • How to install and se up MongoDB server
  • How to interact manually with database server via Mongo REPL
  • How to install and use MongoDB Node.js Driver to communicate with MongoDB server from within a Node application.
  • How to use Mongoose ODM to enforce a Schema on documents by creating Models for a collection from a Schema then using the Model when creating documents for that collection

Now we are ready to put the knowledge that we gained from these past two weeks together in the following two exercises.

Look at our example below. We will update the Express Server we created using Express Generator in new campsite server project folder, so that it is able to handle all of the business logic processing as well as issue requests to the MongoDB database over Mongoose and the MongoDB Node Driver.

As an example, let us say that we have a GET request coming into the Express web server. A GET request means that the client wants to retrieve data from the EXPRESS server, whether it is a static file such as aboutus.html or data stored in a database. When the Express Server receives this request then it will figure out if it’s a request for a static asset or a database request. Let us say it is for a database request, say for a list of all the campsites that are in the database. The express server then performs what is referred to as the business logic on its side such as figuring out how is that data going to be represented making sure that it’s a valid request and handling any errors, what to include in the response header, as well as checking for proper authorization if needed and other such considerations in figuring out how to respond to the request from the client. Then if it needs to perform some database operation like (CRUD) creating, reading, updating or deleting an entry in the database, then the Express Server must take on the role of a client itself as a client to the MongoDB database server because the Express application will be the entity making a request to a server. And it is able to do that through the MongoDB Node Driver, which we wrap with Mongoose in order to enforce Schemas. Then once it has completed the database operation then the Express application once again takes on the role of a server and sends a response back to the HTTP client that originated this entire cycle typically that would be a web browser, but it could also be another application such as POSTMAN. So the takeaway is that any time that the Express application receives an HTTP request like GET, PUT, POST, or DELETE from the client and it’s not about a static file but about data that is stored in a Database, then the Express application needs to initiate a corresponding database operation behind the scenes, for which it will then act as the client to the database server. The HTTP client does not have to handle any of this complicated stuff. All it does it says to the Express Server for example, hey get me the campsite’s data, then the Express Server has to figure out how to get the data from MongoDB and handle any errors, package the data in a way that the client can understand it and send the data back to the client. So this is the final piece of the puzzle that we will be implementing in the next two exercises. We will bring it all together: the Express Application, the Express Routers, and REST APi endpoints that we defined in them, the MongoDB database server and the node driver to communicate with it, wrapped in the Mongoose ODM and the Mongoose Schemas using the Campsite’s data that we have been working with so far.

.


REST API with Express, MongoDB, and Mongoose Part 1

  • Develop a full-fledged REST API server with Express, MongoDB, and Mongoose.
  • Serve various REST API endpoints that interact with the MongoDB server.

Exercise Resources

db.json

Instructions

  • Copy the models folder from the node-mongoose folder to the nucampsiteServer folder you created before.
  • Then install mongoose and mongoose-currency in the nucampsiteServer folder:   npm install mongoose@5.10.9 mongoose-currency@0.2.0

Update the Express application

  • Open the app.js file and add in the code to connect to the MongoDB server as follows:
var createError = require(‘http-errors’);
var express = require(‘express’);
var path = require(‘path’);
var cookieParser = require(‘cookie-parser’);
var logger = require(‘morgan’);
var indexRouter = require(‘./routes/index’);
var usersRouter = require(‘./routes/users’);
const campsiteRouter = require(‘./routes/campsiteRouter’);
const promotionRouter = require(‘./routes/promotionRouter’);
const partnerRouter = require(‘./routes/partnerRouter’);
const mongoose = require(‘mongoose’);
const url = ‘mongodb://localhost:27017/nucampsite’;
const connect = mongoose.connect(url, {
    useCreateIndex: true,
    useFindAndModify: false,
    useNewUrlParser: true, 
    useUnifiedTopology: true
});
connect.then(() => console.log(‘Connected correctly to server’), 
    err => console.log(err)
);
var app = express();
// view engine setup
app.set(‘views’, path.join(__dirname, ‘views’));
app.set(‘view engine’, ‘jade’);
app.use(logger(‘dev’));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, ‘public’)));
app.use(‘/’, indexRouter);
app.use(‘/users’, usersRouter);
app.use(‘/campsites’, campsiteRouter);
app.use(‘/promotions’, promotionRouter);
app.use(‘/partners’, partnerRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get(‘env’) === ‘development’ ? err : {};
  // render the error page
  res.status(err.status || 500);
  res.render(‘error’);
});
module.exports = app;

Update the Mongoose Schema/Model

  • Next open campsite.js in the nucampsiteServer/models folder and update it as follows:
const mongoose = require(‘mongoose’);
const Schema = mongoose.Schema;
require(‘mongoose-currency’).loadType(mongoose);
const Currency = mongoose.Types.Currency;
const commentSchema = new Schema({
    rating: {
        type: Number,
        min: 1,
        max: 5,
        required: true
    },
    text: {
        type: String,
        required: true
    },
    author: {
        type: String,
        required: true
    }
}, {
    timestamps: true
});
const campsiteSchema = new Schema({
    name: {
        type: String,
        required: true,
        unique: true
    },
    description: {
        type: String,
        required: true
    },
    image: {
        type: String,
        required: true
    },
    elevation: {
        type: Number,
        required: true
    },
    cost: {
        type: Currency,
        required: true,
        min: 0
    },
    featured: {
        type: Boolean,
        default: false
    },
    comments: [commentSchema]
}, {
    timestamps: true
});
const Campsite = mongoose.model(‘Campsite’, campsiteSchema);
module.exports = Campsite;

Update the Express Router

  • Now open routes/campsiteRouter.js and update its code as follows:
const express = require('express');
const Campsite = require('../models/campsite');

const campsiteRouter = express.Router();

campsiteRouter.route('/')
.get((req, res, next) => {
    Campsite.find()
    .then(campsites => {
        res.statusCode = 200;
        res.setHeader('Content-Type', 'application/json');
        res.json(campsites);
    })
    .catch(err => next(err));
})
.post((req, res, next) => {
    Campsite.create(req.body)
    .then(campsite => {
        console.log('Campsite Created ', campsite);
        res.statusCode = 200;
        res.setHeader('Content-Type', 'application/json');
        res.json(campsite);
    })
    .catch(err => next(err));
})
.put((req, res) => {
    res.statusCode = 403;
    res.end('PUT operation not supported on /campsites');
})
.delete((req, res, next) => {
    Campsite.deleteMany()
    .then(response => {
        res.statusCode = 200;
        res.setHeader('Content-Type', 'application/json');
        res.json(response);
    })
    .catch(err => next(err));
});

campsiteRouter.route('/:campsiteId')
.get((req, res, next) => {
    Campsite.findById(req.params.campsiteId)
    .then(campsite => {
        res.statusCode = 200;
        res.setHeader('Content-Type', 'application/json');
        res.json(campsite);
    })
    .catch(err => next(err));
})
.post((req, res) => {
    res.statusCode = 403;
    res.end(`POST operation not supported on /campsites/${req.params.campsiteId}`);
})
.put((req, res, next) => {
    Campsite.findByIdAndUpdate(req.params.campsiteId, {
        $set: req.body
    }, { new: true })
    .then(campsite => {
        res.statusCode = 200;
        res.setHeader('Content-Type', 'application/json');
        res.json(campsite);
    })
    .catch(err => next(err));
})
.delete((req, res, next) => {
    Campsite.findByIdAndDelete(req.params.campsiteId)
    .then(response => {
        res.statusCode = 200;
        res.setHeader('Content-Type', 'application/json');
        res.json(response);
    })
    .catch(err => next(err));
});

module.exports = campsiteRouter;
  • Start the application. Make sure your MongoDB server is up and running.
  • You can now fire up Postman and test several operations on the REST API. You can use the data for all the campsites provided in the db.json file given above in the Exercise Resources to test your server, The db.json file is meant to provide you with JSON-formatted data that you can copy and paste to send to the server from Postman. It is not used in the execution of your app.
  • Optional: Commit your changes to Git with the message “Express REST API with MongoDB and Mongoose Part 1”
{“name”: “test”, “description”: “test description”}

REST API with Express, MongoDB, and Mongoose Part 2

  • Add support for accessing and updating comments for the campsites.
  • Continuing with the nucampsiteServer project, add the following code to campsiteRouter.js to handle comments:
const express = require(‘express’);
const Campsite = require(‘../models/campsite’);
const campsiteRouter = express.Router();
campsiteRouter.route(‘/’)
.get((req, res, next) => {
    Campsite.find()
    .then(campsites => {
        res.statusCode = 200;
        res.setHeader(‘Content-Type’, ‘application/json’);
        res.json(campsites);
    })
    .catch(err => next(err));
})
.post((req, res, next) => {
    Campsite.create(req.body)
    .then(campsite => {
        console.log(‘Campsite Created ‘, campsite);
        res.statusCode = 200;
        res.setHeader(‘Content-Type’, ‘application/json’);
        res.json(campsite);
    })
    .catch(err => next(err));
})
.put((req, res) => {
    res.statusCode = 403;
    res.end(‘PUT operation not supported on /campsites’);
})
.delete((req, res, next) => {
    Campsite.deleteMany()
    .then(response => {
        res.statusCode = 200;
        res.setHeader(‘Content-Type’, ‘application/json’);
        res.json(response);
    })
    .catch(err => next(err));
});
campsiteRouter.route(‘/:campsiteId’)
.get((req, res, next) => {
    Campsite.findById(req.params.campsiteId)
    .then(campsite => {
        res.statusCode = 200;
        res.setHeader(‘Content-Type’, ‘application/json’);
        res.json(campsite);
    })
    .catch(err => next(err));
})
.post((req, res) => {
    res.statusCode = 403;
    res.end(`POST operation not supported on /campsites/${req.params.campsiteId}`);
})
.put((req, res, next) => {
    Campsite.findByIdAndUpdate(req.params.campsiteId, {
        $set: req.body
    }, { new: true })
    .then(campsite => {
        res.statusCode = 200;
        res.setHeader(‘Content-Type’, ‘application/json’);
        res.json(campsite);
    })
    .catch(err => next(err));
})
.delete((req, res, next) => {
    Campsite.findByIdAndDelete(req.params.campsiteId)
    .then(response => {
        res.statusCode = 200;
        res.setHeader(‘Content-Type’, ‘application/json’);
        res.json(response);
    })
    .catch(err => next(err));
});
campsiteRouter.route(‘/:campsiteId/comments’)
.get((req, res, next) => {
    Campsite.findById(req.params.campsiteId)
    .then(campsite => {
        if (campsite) {
            res.statusCode = 200;
            res.setHeader(‘Content-Type’, ‘application/json’);
            res.json(campsite.comments);
        } else {
            err = new Error(`Campsite ${req.params.campsiteId} not found`);
            err.status = 404;
            return next(err);
        }
    })
    .catch(err => next(err));
})
.post((req, res, next) => {
    Campsite.findById(req.params.campsiteId)
    .then(campsite => {
        if (campsite) {
            campsite.comments.push(req.body);
            campsite.save()
            .then(campsite => {
                res.statusCode = 200;
                res.setHeader(‘Content-Type’, ‘application/json’);
                res.json(campsite);
            })
            .catch(err => next(err));
        } else {
            err = new Error(`Campsite ${req.params.campsiteId} not found`);
            err.status = 404;
            return next(err);
        }
    })
    .catch(err => next(err));
})
.put((req, res) => {
    res.statusCode = 403;
    res.end(`PUT operation not supported on /campsites/${req.params.campsiteId}/comments`);
})
.delete((req, res, next) => {
    Campsite.findById(req.params.campsiteId)
    .then(campsite => {
        if (campsite) {
            for (let i = (campsite.comments.length-1); i >= 0; i–) {
                campsite.comments.id(campsite.comments[i]._id).remove();
            }
            campsite.save()
            .then(campsite => {
                res.statusCode = 200;
                res.setHeader(‘Content-Type’, ‘application/json’);
                res.json(campsite);
            })
            .catch(err => next(err));
        } else {
            err = new Error(`Campsite ${req.params.campsiteId} not found`);
            err.status = 404;
            return next(err);
        }
    })
    .catch(err => next(err));
});
campsiteRouter.route(‘/:campsiteId/comments/:commentId’)
.get((req, res, next) => {
    Campsite.findById(req.params.campsiteId)
    .then(campsite => {
        if (campsite && campsite.comments.id(req.params.commentId)) {
            res.statusCode = 200;
            res.setHeader(‘Content-Type’, ‘application/json’);
            res.json(campsite.comments.id(req.params.commentId));
        } else if (!campsite) {
            err = new Error(`Campsite ${req.params.campsiteId} not found`);
            err.status = 404;
            return next(err);
        } else {
            err = new Error(`Comment ${req.params.commentId} not found`);
            err.status = 404;
            return next(err);
        }
    })
    .catch(err => next(err));
})
.post((req, res) => {
    res.statusCode = 403;
    res.end(`POST operation not supported on /campsites/${req.params.campsiteId}/comments/${req.params.commentId}`);
})
.put((req, res, next) => {
    Campsite.findById(req.params.campsiteId)
    .then(campsite => {
        if (campsite && campsite.comments.id(req.params.commentId)) {
            if (req.body.rating) {
                campsite.comments.id(req.params.commentId).rating = req.body.rating;
            }
            if (req.body.text) {
                campsite.comments.id(req.params.commentId).text = req.body.text;
            }
            campsite.save()
            .then(campsite => {
                res.statusCode = 200;
                res.setHeader(‘Content-Type’, ‘application/json’);
                res.json(campsite);
            })
            .catch(err => next(err));
        } else if (!campsite) {
            err = new Error(`Campsite ${req.params.campsiteId} not found`);
            err.status = 404;
            return next(err);
        } else {
            err = new Error(`Comment ${req.params.commentId} not found`);
            err.status = 404;
            return next(err);
        }
    })
    .catch(err => next(err));
})
.delete((req, res, next) => {
    Campsite.findById(req.params.campsiteId)
    .then(campsite => {
        if (campsite && campsite.comments.id(req.params.commentId)) {
            campsite.comments.id(req.params.commentId).remove();
            campsite.save()
            .then(campsite => {
                res.statusCode = 200;
                res.setHeader(‘Content-Type’, ‘application/json’);
                res.json(campsite);
            })
            .catch(err => next(err));
        } else if (!campsite) {
            err = new Error(`Campsite ${req.params.campsiteId} not found`);
            err.status = 404;
            return next(err);
        } else {
            err = new Error(`Comment ${req.params.commentId} not found`);
            err.status = 404;
            return next(err);
        }
    })
    .catch(err => next(err));
});
module.exports = campsiteRouter;
  • Start the application. Make sure your MongoDB server is up and running.
  • You can now fire up Postman and test several operations on the REST API. To test the server, use the data for all the campsites provided in the db.json file given in the Exercise Resources in the previous exercise. The db.json file is meant to provide you with JSON-formatted data that you can copy and paste to Postman for testing.
  • Optional: Commit your changes to Git with the message “Express REST API with MongoDB and Mongoose Part 2”.

Additional Resources: