Now we need to create the Mongoose models that will be reflect in the MongoDB, this is similar to Entities in the Java world. I have created the directory structure below to keep all the models in one place.
I have created 3 Models User, Profile and Post as seen below, most of the below is self explaining, the only thing to notice is that in MongoDB you can have documents inside documents which I have an example of this in my Post Model where I use the User Model. Models are pretty straight forward and I will leave you to the Mongoose documenaytion for more complex examples.
User Model | const mongoose = require('mongoose'); const UserSchema = new mongoose.Schema({ name: { type: String, required: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, avatar : { type: String }, date: { type: Date, default: Date.now } }); module.exports = User = mongoose.model('user', UserSchema); |
Profile Model | const mongoose = require('mongoose'); const ProfileSchema = new mongoose.Schema({ // reference the User model by id user: { type: mongoose.Schema.Types.ObjectId, ref: 'user' }, company: { type: String }, website: { type: String }, location: { type: String }, status: { type: String, required: true }, skills: { type: [String], required: true }, bio: { type: String }, githubusername: { type: String }, // an array of other fields experience: [ { title: { type: String, required: true }, company: { type: String, required: true }, location: { type: String }, from: { type: Date, required: true }, to: { type: Date }, current: { type: Boolean, default: false }, description: { type: String } } ], education: [ { school: { type: String, required: true }, degree: { type: String, required: true }, fieldofstudy: { type: String, required: true }, from: { type: Date, required: true }, to: { type: Date }, current: { type: Boolean, default: false }, description: { type: String } } ], social: { youtube: { type: String }, twitter: { type: String }, facebook: { type: String }, linkedin: { type: String }, instagram: { type: String } }, // use a default value date: { type: Date, default: Date.now } }); module.exports = Profile = mongoose.model('profile', ProfileSchema); |
Post Model | const mongoose = require('mongoose'); const Schema = mongoose.Schema; const PostSchema = new Schema({ user: { type: Schema.Types.ObjectId, ref: 'users' }, text: { type: String, required: true }, name: { type: String }, avatar: { type: String }, likes: [ { user: { type: Schema.Types.ObjectId, ref: 'users' } } ], comments: [ { user: { type: Schema.Types.ObjectId, ref: 'users' }, text: { type: String, required: true }, name: { type: String }, avatar: { type: String }, date: { type: Date, default: Date.now } } ], date: { type: Date, default: Date.now } }); module.exports = Post = mongoose.model('post', PostSchema); |
By using Mongoose we have a number of methods (called CRUD operations - Create, Read, Update, Delete) we can use to extract data from the MongoDB, below is taken from the Mongoose doc's and you can see there many methods we can use, I will show examples of these throughout the section.
This section I cover validation, however we need to setup a piece of configuration (server.js) that allows use to parse the body of a HTTP request which can be seen below, the express.json is a json body parser, it part of the middleware, the extended of false means that you cannot post nested objects (actually parses using a different library querystring instead of qs)
So a typical POST request would look something like below with data being sent in the body of the request, express than will parse this body to extract out the data
Next we can use the express validator to perform checks and validation on incoming data, add the below line to the users.js file.
The User post route would then look something like below for checking data, to handle the response we see if there are any errors and if there are we return a status of 400 and the list of errors via an array.
So when you send a request that has errors you get the below back, I purposefully triggered all the errors, also notice that the status is 400 (BAD Request)
If you send the correct data then the code below the error checking will then be executed, also you get a status of 200, if you have nothing to execute.
I have added comments to the users.js file to explain what is happening on the complex parts, some of the code is self explain and thus no comments have been added. I will cover JWT in detail in the next section.
User Registration | const express = require('express'); const router = express.Router(); const gravatar = require('gravatar'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const config = require('config'); const {check, validationResult} = require('express-validator'); // User model const User = require('../../models/User'); // @route GET api/users // @desc Test Route // @access Public router.get('/', (req, res) => res.send('User route')); // @route POST api/users // @desc Register user // @access Public router.post( '/', [ // check validation, see below if check if there were errors check('name', 'Name is required').not().isEmpty(), check('email', 'Please include a valid email').isEmail(), check('password', 'Please enter a password with 6 or more characters').isLength({min: 6}) ], async (req, res) => { const errors = validationResult(req); // Use below to check for errors with data if(!errors.isEmpty()){ // return array with message return res.status(400).json({ errors: errors.array() }); } const { name, email, password } = req.body; // use a try-catch in case something breaks try { // See if the user exists we use a Mongoose CRUD operation findOne() let user = await User.findOne({ email }); if(user) { // return array with message return res.status(400).json({ errors: [{ msg: 'User already exists'}] }); } // Get users gravatar const avatar = gravatar.url(email, { s: '200', r: 'pg', d: 'mm' }); // get a User Model object user = new User({ name, email, avatar, password }); // Encrypt password const salt = await bcrypt.genSalt(10); user.password = await bcrypt.hash(password, salt); // Now save the user to MongoDB using Mongoose CRUD save method await user.save(); // Return JWT const payload = { user: { id: user.id } }; jwt.sign( payload, config.get('jwtSecret'), { expiresIn: 360000 }, (err, token) => { if (err) throw err; res.json({token}); } ); } catch(err) { console.error(err.message); res.status(500).send('Server error'); } } ); module.exports = router; |
If all goes well then you will end up with something like below in the MongoDB
JSON Web Token (JWT) is broken into three parts for example xxxxx.yyyyy.zzzzz
A graphical example of a JWT is below
We are going to use the jsonwebtoken package, which you can go to, to get the full documentation. The code entered in the users.js (full code was shown in the above section) is below, we create a payload, we then create a jwt.sign that takes in the payload and we then create a global variable for the secret token (config/default.json), we pass in some options in this case the expiry time and then create a simple callback.
When we send a postman test request we get the response below back, which is our JWT token.
In order to authenticate using the JWT we need to create some middleware (you could use passport package), I will cover the next statements in a moment
![]() |
![]() |
As the above is a middleware function you need to pass three parameters, the normal res and req objects but a nextobject which is used to call the next piece of middleware, If the current middleware function does not end the request-response cycle, it must call next() method to pass control to the next middleware function. Otherwise, the request will be left hanging.
Now we can use this to protect the route, we update the auth.js file, we pull in the auth middleware and then add it as a second parameter to the router.get() method as per below, the -password omits the password from the user object.
We can then use postman to test the protected route, we pass the token we got earlier as a x-auth-token and send a GET request and you should be something like below
I am not going to go into too much detail as most of the code we have covered anyway, I have commented were we have not covered. There are alots of mongoose CRUD examples and cover HTTP GET, POST, PUT, DELETE, etc. I even have a github request example as well (see bottom of profile).
Profile | const express = require('express'); const request = require('request'); const router = express.Router(); const auth = require('../../middleware/auth'); const {check, validationResult} = require('express-validator'); // Bring in the models const Profile = require('../../models/Profile'); const User = require('../../models/User'); const Post = require('../../models/Post'); // @route GET api/profile/me // @desc Get current users profile // @access Private router.get('/me', auth, async (req, res) => { try { // get the user by id, we can use populate to also bring in the user name and avatar from the User Model const profile = await Profile.findOne({user: req.user.id}).populate('user', ['name', 'avatar']); if (!profile) { return res.status(400).json({msg: 'There is no profile for this user'}); } res.json(profile); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } }); // @route POST api/profile // @desc Create or Update a users profile // @access Private router.post( '/', [ auth, [ check('status', 'Status is required').not().isEmpty(), check('skills', 'Skills is required').not().isEmpty() ] ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({errors: errors.array()}); } // Create a number of fields that will be populated by the request body const { company, website, location, bio, status, githubusername, skills, youtube, facebook, twitter, instagram, linkedin } = req.body; // Build profile object const profileFields = {}; profileFields.user = req.user.id; if (company) profileFields.company = company; if (website) profileFields.website = website; if (location) profileFields.location = location; if (bio) profileFields.bio = bio; if (status) profileFields.status = status; if (githubusername) profileFields.githubusername = githubusername; if (skills) { // need to turn text data into an array profileFields.skills = skills.split(',').map(skill => skill.trim()); } // Build social object profileFields.social = {}; if (youtube) profileFields.social.youtube = youtube; if (twitter) profileFields.social.twitter = twitter; if (facebook) profileFields.social.facebook = facebook; if (linkedin) profileFields.social.linkedin = linkedin; if (instagram) profileFields.social.instagram = instagram; try { let profile = await Profile.findOne({user: req.user.id}); if (profile) { // Update profile = await Profile.findOneAndUpdate( {user: req.user.id}, // update the user with id passed {$set: profileFields}, // will update the fields passed {new: true} // return the updated document by default returns original ); return res.json(profile); } // Create profile = new Profile(profileFields); await profile.save(profile); res.json(profile); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } } ); // @route GET api/profile // @desc Get all profiles // @access Public router.get('/', async (req, res) => { try { const profiles = await Profile.find().populate('user', ['name', 'avatar']); res.json(profiles); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } }); // @route GET api/profile/user/:user_id // @desc Get profile by user ID // @access Public router.get('/user/:user_id', async (req, res) => { try { const profile = await Profile.findOne({user: req.params.user_id}).populate('user', ['name', 'avatar']); if (!profile) { return res.status(400).json({msg: 'Profile not found'}); } res.json(profile); } catch (err) { console.error(err.message); if (err.kind === 'ObjectId') { return res.status(400).json({msg: 'Profile not found'}); } res.status(500).send('Server Error'); } }); // @route DELETE api/profile // @desc Delete profile, user and posts // @access Private router.delete('/', auth, async (req, res) => { try { // remove users posts await Post.deleteMany({user: req.user.id}); // Remove profile await Profile.findOneAndRemove({user: req.user.id}); // Remove user await User.findOneAndRemove({_id: req.user.id}); res.json({msg: 'User and profile removed'}); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } }); // @route PUT api/profile/experience // @desc Add profile experience // @access Private router.put('/experience', [ auth, [ check('title', 'Title is required').not().isEmpty(), check('company', 'Company is required').not().isEmpty(), check('from', 'From date is required').not().isEmpty() ] ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({errors: errors.array()}); } // Load the variables using the data passed by the user const { title, company, location, from, to, current, description } = req.body; // Create a new object using the data above const newExp = { title, company, location, from, to, current, description }; try { const profile = await Profile.findOne({user: req.user.id}); profile.experience.unshift(newExp); await profile.save(); res.json(profile); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } } ); // @route DELETE api/profile/experience/:exp_id // @desc Delete experience from profile // @access Private router.delete('/experience/:exp_id', auth, async (req, res) => { try { const profile = await Profile.findOne({user: req.user.id}); // Get remove index const removeIndex = profile.experience .map(item => item.id) .indexOf(req.params.exp_id); profile.experience.splice(removeIndex, 1); await profile.save(); res.json(profile); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } }); // @route PUT api/profile/education // @desc Add profile education // @access Private router.put( '/education', [ auth, [ check('school', 'School is required').not().isEmpty(), check('degree', 'Degree is required').not().isEmpty(), check('fieldofstudy', 'Field of study is required').not().isEmpty(), check('from', 'From date is required').not().isEmpty() ] ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({errors: errors.array()}); } const { school, degree, fieldofstudy, from, to, current, description } = req.body; const newEdu = { school, degree, fieldofstudy, from, to, current, description }; try { const profile = await Profile.findOne({user: req.user.id}); profile.education.unshift(newEdu); await profile.save(); res.json(profile); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } } ); // @route DELETE api/profile/education/:edu_id // @desc Delete education from profile // @access Private router.delete('/education/:edu_id', auth, async (req, res) => { try { const profile = await Profile.findOne({user: req.user.id}); // Get remove index const removeIndex = profile.education .map(item => item.id) .indexOf(req.params.edu_id); profile.education.splice(removeIndex, 1); await profile.save(); res.json(profile); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } }); // @route GET api/profile/github/:username // @desc Get user repos from Github // @access Public // router.get('/github/:username', (req, res) => { // try { // const options = { // uri: `https://api.github.com/users/${ // req.params.username // }/repos?per_page=5&sort=created:asc&client_id=${config.get( // 'githubClientId' // )}&client_secret=${config.get('githubSecret')}`, // method: 'GET', // headers: { 'user-agent': 'node.js' } // }; // // request(options, (error, response, body) => { // if (error) console.error(error); // // if (response.statusCode !== 200) { // return res.status(404).json({ msg: 'No Github profile found' }); // } // // res.json(JSON.parse(body)); // }); // } catch (err) { // console.error(err.message); // res.status(500).send('Server Error'); // } // }); module.exports = router; |
Some more example regarding the posts, you probably can see a recurring theme
Post examples | const express = require('express'); const router = express.Router(); const { check, validationResult } = require('express-validator'); const auth = require('../../middleware/auth'); const Post = require('../../models/Post'); const Profile = require('../../models/Profile'); const User = require('../../models/User'); // @route POST api/posts // @desc Create a post // @access Private router.post( '/', [ auth, [ check('text', 'Text is required').not().isEmpty() ] ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } try { const user = await User.findById(req.user.id).select('-password'); const newPost = new Post({ text: req.body.text, name: user.name, avatar: user.avatar, user: req.user.id }); const post = await newPost.save(); res.json(post); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } } ); // @route GET api/posts // @desc Get all posts // @access Private router.get('/', auth, async (req, res) => { try { const posts = await Post.find().sort({ date: -1 }); res.json(posts); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } }); // @route GET api/posts/:id // @desc Get post by ID // @access Private router.get('/:id', auth, async (req, res) => { try { const post = await Post.findById(req.params.id); if (!post) { return res.status(404).json({ msg: 'Post not found' }); } res.json(post); } catch (err) { console.error(err.message); if (err.kind === 'ObjectId') { return res.status(404).json({ msg: 'Post not found' }); } res.status(500).send('Server Error'); } }); // @route DELETE api/posts/:id // @desc Delete a post // @access Private router.delete('/:id', auth, async (req, res) => { try { const post = await Post.findById(req.params.id); if (!post) { return res.status(404).json({ msg: 'Post not found' }); } // Check user if (post.user.toString() !== req.user.id) { return res.status(401).json({ msg: 'User not authorized' }); } await post.remove(); res.json({ msg: 'Post removed' }); } catch (err) { console.error(err.message); if (err.kind === 'ObjectId') { return res.status(404).json({ msg: 'Post not found' }); } res.status(500).send('Server Error'); } }); // @route PUT api/posts/like/:id // @desc Like a post // @access Private router.put('/like/:id', auth, async (req, res) => { try { const post = await Post.findById(req.params.id); // Check if the post has already been liked if ( post.likes.filter(like => like.user.toString() === req.user.id).length > 0 ) { return res.status(400).json({ msg: 'Post already liked' }); } post.likes.unshift({ user: req.user.id }); await post.save(); res.json(post.likes); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } }); // @route PUT api/posts/unlike/:id // @desc Like a post // @access Private router.put('/unlike/:id', auth, async (req, res) => { try { const post = await Post.findById(req.params.id); // Check if the post has already been liked if ( post.likes.filter(like => like.user.toString() === req.user.id).length === 0 ) { return res.status(400).json({ msg: 'Post has not yet been liked' }); } // Get remove index const removeIndex = post.likes .map(like => like.user.toString()) .indexOf(req.user.id); post.likes.splice(removeIndex, 1); await post.save(); res.json(post.likes); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } }); // @route POST api/posts/comment/:id // @desc Comment on a post // @access Private router.post( '/comment/:id', [ auth, [ check('text', 'Text is required').not().isEmpty() ] ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } try { const user = await User.findById(req.user.id).select('-password'); const post = await Post.findById(req.params.id); const newComment = { text: req.body.text, name: user.name, avatar: user.avatar, user: req.user.id }; post.comments.unshift(newComment); await post.save(); res.json(post.comments); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } } ); // @route DELETE api/posts/comment/:id/:comment_id // @desc Delete comment // @access Private router.delete('/comment/:id/:comment_id', auth, async (req, res) => { try { const post = await Post.findById(req.params.id); // Pull out comment const comment = post.comments.find( comment => comment.id === req.params.comment_id ); // Make sure comment exists if (!comment) { return res.status(404).json({ msg: 'Comment does not exist' }); } // Check user if (comment.user.toString() !== req.user.id) { return res.status(401).json({ msg: 'User not authorized' }); } // Get remove index const removeIndex = post.comments .map(comment => comment.id) .indexOf(req.params.comment_id); post.comments.splice(removeIndex, 1); await post.save(); res.json(post.comments); } catch (err) { console.error(err.message); res.status(500).send('Server Error'); } }); module.exports = router; |