Creating a REST API Backend using Express.js

Antonio Erdeljac
17 min readSep 17, 2017

--

This is the Medium.com version of the original article I created on LinkedIn. You can see it here: LinkedIn version

Hello, my name is Antonio Erdeljac, and I am a 16 year old JavaScript developer. This is my first ever article and tutorial.

In this article I will be creating a REST API Backend using Node’s Express.js. I want to stress that I have bought a Thinkser course to learn how to do this, and I will use this tutorial to provide free info to you and increase my skills.

Since this is my first tutorial ever, I highly recommend good JavaScript knowledge since I doubt I will do very good at explaining what I am doing. It is expected to also know how to work in Postman (sending POST, PUT, DELETE requests and setting the Headers). Also, some experience with MongoDB would be good, but not entirely necessary.

Bootstraping our project

We will start by bootstraping our project using express-generate with a few modifications. You can download the starter project from here: Starter project

After you’ve uncompressed the project, open it and run npm-install command in the specified directory (where app.js, config, models etc. are)

Let’s get started.

1. Establishing Nodemon

Let’s start by applying Nodemon to our project so our server automatically updates every time we change a line of code.

In package.json do the following changes:

"scripts": {
"start": "node ./app.js",
//Changes
"dev": "nodemon ./app.js",
//Changes over
"test": "echo \"Error: no test specified\" && exit 1"
},

What this will allow us is to run the npm run dev command in the terminal and Nodemon is going to start our server. Run npm run dev in terminal to start the server and go to the next step.

2. Creating the User model

We are going to use Mongoose library to easily create Schemas and Models for Users, Articles, Comments etc.

In Models folder create a new file called User.js

Let’s add some first lines of code to User.js:

var mongoose = require('mongoose');
var uniqueValidator = require('mongoose-unique-validator');
var crypto = require('crypto');
var jwt = require('jsonwebtoken');
var secret = require('../config').secret;
var UserSchema = new mongoose.Schema({
username: {type: String, unique: true, required: [true, "cannot be empty."], lowercase: true, index: true},
email: {type: String, unique: true, required: [true, "cannot be empty."], lowercase: true, index: true},
bio: String,
image: String,
salt: String,
hash: String
}, {timestamps: true});

Great, we have just created our first Schema using Mongoose. We defined user’s username to be a required, unique and a lowercase string. We did the same thing for e-mail as well. For other properties we have simply set them to be Strings.

Let’s apply uniqueValidator library to our User.js file:

UserSchema.plugin(uniqueValidator, {message: "is already taken."});

Let’s add some methods to User.js

...var UserSchema = new mongoose.Schema({
username: {type: String, unique: true, required: [true, "cannot be empty."], lowercase: true, index: true},
email: {type: String, unique: true, required: [true, "cannot be empty."], lowercase: true, index: true},
bio: String,
image: String,
salt: String,
hash: String
}, {timestamps: true});
UserSchema.plugin(uniqueValidator, {message: "is already taken."});
//^ OLD CODE, just so know where we left off. ^
//NEW CODE BENEATH THIS COMMENTUserSchema.methods.setPassword = function(password){
this.salt = crypto.randomBytes(16).toString('hex');
this.hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
};
UserSchema.methods.validPassword = function(password){
var hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
return this.hash === hash;
};
UserSchema.methods.generateJWT = function(){
var today = new Date();
var exp = new Date(today);
exp.setDate(today.getDate()+60);
return jwt.sign({
id: this._id,
username: this.username,
exp: parseInt(exp.getTime()/1000)
}, secret)
};
UserSchema.methods.toAuthJSON = function(){
return {
username: this.username,
email: this.email,
bio: this.bio,
image: this.image,
token: this.generateJWT()
};
};

Great, we’ve now defined the following methods:

  1. setPassword — used to generate a password by randomly creating user’s hash & salt properties to encrypt a password provided by user using Crypto library
  2. validPassword — used to compare the provided password with the users’ actual password
  3. generateJWT — used to create a JSON Web Token that expires 60 days from creation and will be stored and used in the Frontend’s window.localStorage()
  4. toAuthJSON — used to return specified user’s properties (username, email, bio…)

Lastly, let’s add the UserSchema to mongoose’s models.

mongoose.model('User', UserSchema);

Now let’s require User model in app.js (find the line “app.use(require(‘./routes’));” and do the following changes):

...
//NEW LINE OF CODE BENEATH THIS COMMENT

require('./models/User');
//OLD CODE BENEATH THIS COMMENT

app.use(require('./routes'));
/// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
...

Great, server should now register User model and you should see the message in terminal that will look like this:

Listening on port 8000
Mongoose: users.ensureIndex({ username: 1 }) { unique: true, background: true }
Mongoose: users.ensureIndex({ email: 1 }) { unique: true, background: true }

If you have the exact message in terminal, congrats, you’ve finished your first model in this tutorial!

3. Creating the passport

We are now going to create a passport which we will use in user authentication.

In config folder create a new file called passport.js with the following code:

var passport = require('passport');
var LocalStrategy = require('passport-local');
var mongoose = require('mongoose');
var User = mongoose.model('User');
passport.use(new LocalStrategy({
usernameField: 'user[email]',
passwordField: 'user[password]'
}, function(email, password, done){
User.findOne({email: email}).then(function(user){
if(!user || !user.validPassword(password)){
return done(null, false, {errors: {"email or password":"is invalid."}})
}
return done(null, user);
}).catch(done);
}));

We will use this when authenticating our user’s login info, if email and password are correct, the user model will be returned, otherwise an error is going to be returned saying “email or password is invalid.”

Register the passport.js in app.js:

...
require('./models/User');
//OLD Code on top, just so you can know where we left off.
//new code beneath this comment

require('./config/passport');
//old code beneath this commentapp.use(require('./routes'));

Great, you’ve now created the passport and successfully registered it to App. Check the terminal for errors (There shouldn’t be any) and go to the next step.

4. Creating the auth route

We are going to create a file responsible for handling whether user is required to be logged in or not logged in.

In routes/ folder create a new file called auth.js with the following code:

var jwt = require('express-jwt');
var secret = require('../config').secret;
function getTokenFromHeaders(req){
if(req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Token'){
return req.headers.authorization.split(' ')[1];
}
return null;
}
var auth = {
required: jwt({
secret: secret,
userProperty: 'payload',
getToken: getTokenFromHeaders
}),
optional: jwt({
secret: secret,
userProperty: 'payload',
credentialsRequired: false,
getToken: getTokenFromHeaders
})
};
module.exports = auth;

Let’s explain what this does.

Function getTokenFromHeaders is going to check whether there is a Token provided in the sent request’s header, and return it if there is one, otherwise return null.

Auth object is going to be called in our routes depending if we want user to be required to be logged in to see the specific route or do action to it, or if we don’t care (optional) if he is logged in or not.

5. Creating the users route

We are now going to create a route that is going to handle registering the user, logging in the user, updating the user etc.

In routes/api/ folder create a new file called users.js.

Let’s start by creating the ability to register an user. Do the following changes to users.js:

var mongoose = require('mongoose');
var router = require('express').Router();
var auth = require('../auth');
var User = mongoose.model('User');
var passport = require('passport');
router.post('/users', function(req,res,next){
var user = new User();
user.username = req.body.user.username;
user.email = req.body.user.email;
user.setPassword(req.body.user.password);
user.save().then(function(){
return res.json({user: user.toAuthJSON()});
}).catch(next);
});

As you can see we are using User model and settings it’s username, email, and password properties, after that we are saving it and returning it using the function we created user.toAuthJSON().

Let’s now add the ability to login. Add the following changes to the users.js:

...router.post('/users/login', function(req,res,next){
if(!req.body.user.email){
return res.status(422).json({errors: {email: "can't be blank."}});
}
if(!req.body.user.password){
return res.status(422).json({errors: {password: "can't be blank."}});
}
passport.authenticate('local', {session: false}, function(err, user, info){
if(err){return next(err);}
if(user){
user.token = user.generateJWT();
return res.json({user: user.toAuthJSON()});
} else {
return res.status(422).json(info);
}
})(req,res,next)
});

We start by checking if user has provided an email and a password, if not, we are returning an error immediately. If everything is good, we use passport to authenticate the user, return errors if any, and more importantly, return the logged in user.

Let’s add the ability for user to check it’s own profile/user model. Add the following changes:

router.get('/user', auth.required, function(req,res,next){
User.findById(req.payload.id).then(function(user){
if(!user){return res.sendStatus(401);}
return res.json({user: user.toAuthJSON()});
}).catch(next);
});

User can now see his info, but only if he is logged in (auth.required).

Lastly, let’s add the ability for an user to update his info. Add the following changes to the code:

router.put('/user', auth.required, function(req,res,next){
User.findById(req.payload.id).then(function(user){
if(!user){return res.sendStatus(401);}
if(typeof req.body.user.username !== 'undefined'){
user.username = req.body.user.username;
}
if(typeof req.body.user.email !== 'undefined'){
user.email = req.body.user.email;
}
if(typeof req.body.user.bio !== 'undefined'){
user.bio = req.body.user.bio;
}
if(typeof req.body.user.image !== 'undefined'){
user.image = req.body.user.image;
}
if(typeof req.body.user.password !== 'undefined'){
user.setPassword(req.body.user.password);
}
return user.save().then(function(){
return res.json({user: user.toAuthJSON()});
});
}).catch(next);
});

User can now update his info, as long as he is logged in. We are also checking if user’s new updates are valid, and leave unchanged those which aren’t.

Let’s create a middleware that will show us the errors (Email already taken, incorrect info, etc.). Add the following changes to users.js:

router.use(function(err,req,res,next){    if(err.name === 'ValidationError'){        return res.json({            errors: Object.keys(err.errors).reduce(function(errors ,key){                errors[key] = err.errors[key].message;                return errors;            }, {})        })    }    return next(err);});

Don’t get scared by this, all it does is it sorts the messy errors object to be a simple {errors: {email: “can’t be blank.”}} type of object.

End the users.js file by adding the last line of code:

module.exports = router;

Lastly, go to the index.js in the same folder users.js is and do the following:

var router = require('express').Router();
// existing code on top
//new coderouter.use('/', require('./users'));
//existing code beneath this comment
module.exports = router;

1. Great, now this is an example of what we send to register the user:

Using Postman send a POST request with the following body to localhost:8000/api/users route:

{
"user":{
"username":"Test",
"email":"test@gmail.com",
"password":"test"
}
}

2. This is an example of what we send to login the user:

Using Postman send a POST request with the following body to localhost:8000/api/users/login route:

{
"user":{
"email":"test@gmail.com",
"password":"test"
}
}

3. This is an example of what we send to get the logged in user:

Note — make sure to copy the token you get when logging in or registering and go to Postman’s Headers settings. Add Authorization and set it to “Token exempletoken” (without quotation marks) It should look like this:

Using Postman send a GET request to localhost:8000/api/user route

4. This is an example of what we send to update the logged in user’s info:

Note — make sure to copy the token you get when logging in or registering and go to Postman’s Headers settings. Add Authorization and set it to “Token exempletoken”

Using Postman send a PUT request with the following body to localhost:8000/api/user route:

{
"user":{
"email":"newemail@gmail.com",
"username":"newusername"
}
}

you should now get returned new, updated user.

6. Creating the Profiles

Let’s go back to models/User.js and add a new method:

UserSchema.methods.toProfileJSONFor = function(user){
return {
username: this.username,
bio: this.bio,
image: this.image,
following: false // we will change this later
};
};

We will call this to get only the info user would want another user to see, without his token and email.

Now let’s go to routes/api/ and create a new file called profiles.js

Let’s create a middleware that will check if an user exists anytime /profiles/someUser is called. Add the following to profiles.js:

var mongoose = require('mongoose');
var User = mongoose.model('User');
var router = require('express').Router();
var auth = require('../auth');
router.param('username', function(req,res,next,username){
User.findOne({username: username}).then(function(user){
if(!user){return res.sendStatus(404);}
req.profile = user; return next();
}).catch(next);
});

To explain what this does, anytime we request a /:username route, this middeware is going to check if that :username exist, or if it does not it is going to throw a 404 error.

Let’s add a route that will return the specified profile if it exists:

router.get('/:username', auth.optional, function(req,res,next){
if(req.payload){
User.findById(req.payload.id).then(function(user){
if(!user){return res.json({profile: req.profile.toProfileJSONFor(false)})}
return res.json({profile: req.profile.toProfileJSONFor(user)});
}).catch(next);
} else {
return res.json({profile: req.profile.toProfileJSONFor(false)});
}
});

We are checking whether user visiting a profile is logged in or not, and passing the logged in user to toProfileJSONFor(user), or not passing if it is not logged in toProfileJSONFor(false).

Don’t forget to add the last line:

module.exports = router;

Now go to index.js and do the same thing we did for users.js:

router.use('/profiles', require('./profiles'));

Now go to Postman and send a GET request to localhost:8000/api/profiles/test (or localhost:8000/api/profiles/any_name_in_database)

7. Creating the Article model

In models/ folder create a new file called Article.js.

Let’s start by creating a schema for it. Just like we did for User model.

var mongoose = require('mongoose');
var uniqueValidator = require('mongoose-unique-validator');
var slug = require('slug');
var User = mongoose.model('User');
var ArticleSchema = new mongoose.Schema({
slug: {type: String, lowercase: true, unique: true},
title: String,
description: String,
body: String,
tagList:[{type: String}],
favoritesCount: {type: Number, default: 0},
author: {type: mongoose.Schema.Types.ObjectId, ref:'User'}
}, {timestamps: true});

Lets add uniqueValidator plugin to it

ArticleSchema.plugin(uniqueValidator, {message: "is already taken."});

Let’s implement slugify method which will create an unique slug (example: new-article-title-7aXm9cms)

ArticleSchema.methods.slugify = function(){
this.slug = slug(this.title) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36);
};

Let’s make sure that the slug is always set.

ArticleSchema.pre('validate', function(next){
if(!this.slug){
this.slugify();
}
return next();
});

And the last method, toJSONFor(user) which will return article’s title, slug, body etc…

ArticleSchema.methods.toJSONFor = function(user){
return {
slug: this.slug,
title: this.title,
description: this.description,
body: this.body,
tagList: this.tagList,
favoritesCount: this.favoritesCount,
favorited: user ? user.isFavorite(this._id) : false,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
author: this.author.toProfileJSONFor(user)
};
};

Don’t forget to create the model at the last line in Article.js

mongoose.model('Article', ArticleSchema);

Lastly, go to app.js and add Article model to it.

require('./models/User');/*New code*/ require('./models/Article');require('./config/passport');
app.use(require('./routes'));

Great! check the terminal, if there are any errors, make sure you didn’t make any typos and try to fix them until you get a message like this:

[nodemon] 1.11.0
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node ./app.js`
Listening on port 8000
Mongoose: users.ensureIndex({ username: 1 }) { unique: true, background: true }
Mongoose: users.ensureIndex({ email: 1 }) { unique: true, background: true }

8. Article routes

In routes/api/ create a new file called articles.js

Let’s start by creating a route for creating an article, this will work only if user is logged in, becase we need to set an author for an article (article.author = user).

var mongoose = require('mongoose');
var router = require('express').Router();
var Article = mongoose.model('Article');
var User = mongoose.model('User');
var auth = require('../auth');
router.post('/', auth.required, function(req,res,next){
User.findById(req.payload.id).then(function(user){
if(!user){return res.sendStatus(401);}
var article = new Article(req.body.article);
article.author = user;
return article.save().then(function(){
return res.json({article: article.toJSONFor(user)})
});
}).catch(next);
});

Let’s create a middleware which will return a 404 error if :article is non existent every time we search for :article and set it to req.article it if it exists.

router.param('article', function(req,res,next,slug){
Article.findOne({slug: slug})
.populate('author')
.then(function(article){
if(!article){
return res.sendStatus(404);
}
req.article = article;
return next();
}).catch(next);
});

Creating a route for getting a specified article by slug

router.get('/:article', auth.optional, function(req,res,next){
Promise.all([
req.payload ? User.findById(req.payload.id) : null,
req.article.populate('author').execPopulate()
]).then(function(results){
var user = results[0];
return res.json({article: req.article.toJSONFor(user)});
}).catch(next);
});

What we do here is use a Promise for Async action which checks if there is a logged in user and populates the autor field of req.article (article found by :article), and returns the article displayed for user, or not (if there is no logged in user).

Now let’s create a route which is going to enable updating the article:

router.put('/:article', auth.required, function(req,res,next){
User.findById(req.payload.id).then(function(user){
if(req.article.author._id.toString() === req.payload.id.toString()){
if(typeof req.body.article.title !== 'undefined'){
req.article.title = req.body.article.title;
}
if(typeof req.body.article.description !== 'undefined'){
req.article.description = req.body.article.description;
}
if(typeof req.body.article.body !== 'undefined'){
req.article.body = req.body.article.body;
}
return req.article.save().then(function(){
return res.json({article: req.article.toJSONFor(user)});
});
} else {
return res.sendStatus(403);
}
}).catch(next);
});

A logged in user is required here, because we need to allow updating only to the author of the article. If user is not logged in or the user is not the author, a 403 error is thrown.

Similarly, we are going to create a route that enables deleting the article:

router.delete('/:article', auth.required, function(req,res,next){
User.findById(req.payload.id).then(function(user){
if(req.article.author._id.toString() === req.payload.id.toString()){
req.article.remove().then(function(){
return res.sendStatus(204);
});
} else {
return res.sendStatus(403);
}
}).catch(next);
});

A logged in user is required here as well, for the same reasons.

Don’t forget to write the last line:

module.exports = router;

Lastly, go to index.js in the same folder and add articles.js:

router.use('/', require('./users'));
router.use('/profiles', require('./profiles'));
/*new code*/ router.use('/articles', require('./articles'));

Do the route testing using Postman.

9. Comments

Let’s start by creating Comment model. In models/ add a new file Comment.js

var mongoose = require('mongoose');var CommentSchema = new mongoose.Schema({
body: String,
author: {type: mongoose.Schema.Types.ObjectId, ref:'User'},
article: {type: mongoose.Schema.Types.ObjectId, ref:'Article'}
}, {timestamps: true});
CommentSchema.methods.toJSONFor = function(user){
return {
id: this._id,
body: this.body,
author: this.author.toProfileJSONFor(user)
};
};
mongoose.model('Comment', CommentSchema);

A very simple schema for a simple model.

Don’t forget to add Comment.js to app.js

require('./models/User');
require('./models/Article');
/*new code*/require('./models/Comment');require('./config/passport');
app.use(require('./routes'));

Don’t change anything except /*new code*/ line.

Now lets go back to Article.js (Model) and do some changes.

var ArticleSchema = new mongoose.Schema({
slug: {type: String, lowercase: true, unique: true},
title: String,
description: String,
body: String,
tagList:[{type: String}],
favoritesCount: {type: Number, default: 0},
author: {type: mongoose.Schema.Types.ObjectId, ref:'User'},
/*new code*/comments: [{type: mongoose.Schema.Types.ObjectId, ref:'Comment'}]}, {timestamps: true});

Leave everything unchanged except /*new code*/ line. This adds Comment models as an array inside Article.

Let’s go back to articles.js in routes/api/articles.js and add some code.

Start by importing Comment model on the top

var Comment = mongoose.model('Comment');

Now let’s create a new route that allows us to create a comment on an existing article, if we are logged in.

router.post('/:article/comments', auth.required, function(req,res,next){
User.findById(req.payload.id).then(function(user){
if(!user){return res.sendStatus(401);}
var comment = new Comment(req.body.comment);
comment.author = user;
comment.article = req.article;
comment.save();
req.article.comments.push(comment);
return req.article.save().then(function(){
return res.json({comment: comment.toJSONFor(user)});
});
}).catch(next);
});

Both req.article and logged in user need to exist here because we need to set Comment’s author and article.

Let’s create a route that is going to display all comments in an article:

router.get('/:article/comments', auth.optional, function(req,res,next){
Promise.resolve(req.payload ? User.findById(req.payload.id) : null).then(function(user){
return req.article.populate({
path: 'comments',
populate: {
path: 'author'
},
options: {
sort: {
createdAt: 'desc'
}
}
}).execPopulate().then(function(){
return res.json({comments: req.article.comments.map(function(comment){
return comment.toJSONFor(user);
})});
});
}).catch(next);
});

You are already familiar with Promise now, so this should be pretty clear that it is an async action which populates the comment’s author and sorts the comments by date.

Let’s create a middleware which is going to throw a 404 error if :comment does not exist, and set it to req.comment if it exists.

router.param('comment', function(req,res,next,id){
Comment.findById(id).then(function(comment){
if(!comment){return res.sendStatus(404);}
req.comment = comment; return next();
}).catch(next);
});

Finally let’s create a route which is going to enable comment deletion for the author of the comment:

router.delete('/:article/comments/:comment', auth.required, function(req,res,next){
User.findById(req.payload.id).then(function(user){
if(req.comment.author._id.toString() === req.payload.id.toString()){
req.article.comments.remove(req.comment._id);
return req.article.save()
.then(Comment.findOne({_id: req.comment._id}).remove().exec())
.then(function(){
return res.sendStatus(204);
});
} else {
return res.sendStatus(403);
}
}).catch(next);
});

Test the comment routes using Postman, GET, & DELETE comments with comment’s ID.

10. Favoriting an article

Let’s go back to models/User.js and do some changes.

var UserSchema = new mongoose.Schema({
username: {type: String, lowercase: true, required: [true, "can't be blank."], unique: true, index: true},
email: {type: String, lowercase: true, required: [true, "can't be blank."], unique: true, index: true},
bio: String,
image: String,
salt: String,
hash: String,
/*new code*/favorites: [{type: mongoose.Schema.Types.ObjectId, ref:'Article'}],}, {timestamps: true});

Do not edit anything except /*new code*/ line.

Let’s add some methods to User model to interact with the newly created favorites array.

UserSchema.methods.favorite = function(id){
if(this.favorites.indexOf(id) === -1){
this.favorites.push(id);
}
return this.save();
};
UserSchema.methods.unfavorite = function(id){
this.favorites.remove(id);
return this.save();
};
UserSchema.methods.isFavorite = function(id){
return this.favorites.some(function(favoriteId){
return id.toString() === favoriteId.toString();
});
};

Each line is pretty self — explanatory, first 2 methods (favorite and unfavorite) must end with return this.save();

Let’s go back to models/Article.js and add a method:

Don’t forget to import User model on the top:

var User = mongoose.model('User');

Now you can use it to create a method:

ArticleSchema.methods.updateFavoriteCount = function(){
var article = this;
return User.count({favorites: {$in: [article._id]}}).then(function(count){
article.favoritesCount = count;
return article.save();
});
};

This methods returns the number of people (Users) who have a specific article in their favorites array.

Change the toJSONFor method with the following updates:

ArticleSchema.methods.toJSONFor = function(user){
return {
slug: this.slug,
title: this.title,
description: this.description,
body: this.body,
tagList: this.tagList,
favoritesCount: this.favoritesCount,
/*new code*/ favorited: user ? user.isFavorite(this._id) : false, createdAt: this.createdAt,
updatedAt: this.updatedAt,
author: this.author.toProfileJSONFor(user)
};
};

We can now go back to /routes/api/articles.js and add new routes:

router.post('/:article/favorite', auth.required, function(req,res,next){
User.findById(req.payload.id).then(function(user){
if(!user){return res.sendStatus(401);}
return user.favorite(req.article._id).then(function(){
return req.article.updateFavoriteCount().then(function(){
return res.json({article: req.article.toJSONFor(user)});
});
});
}).catch(next);
});
router.delete('/:article/favorite', auth.required, function(req,res,next){
User.findById(req.payload.id).then(function(user){
if(!user){return res.sendStatus(401);}
return user.unfavorite(req.article._id).then(function(){
return req.article.updateFavoriteCount().then(function(){
return res.json({article: req.article.toJSONFor(user)});
})
});
}).catch(next);
});

Both routes have the same path and param, but a diffrent request type (POST & DELETE). What these routes do is call the functions we defined moments ago in User.js and Article.js.

Test the favoriting / unfavoriting functions in Postman.

11. Following Users

Let’s go back to /model/User.js and do some changes to the schema:

var UserSchema = new mongoose.Schema({
username: {type: String, lowercase: true, required: [true, "can't be blank."], unique: true, index: true},
email: {type: String, lowercase: true, required: [true, "can't be blank."], unique: true, index: true},
bio: String,
image: String,
salt: String,
hash: String,
favorites: [{type: mongoose.Schema.Types.ObjectId, ref:'Article'}],
/*new code*/following: [{type: mongoose.Schema.Types.ObjectId, ref:'User'}]}, {timestamps: true});

Now let’s add some new methods to User.js which will interact with following array

UserSchema.methods.follow = function(id){
if(this.following.indexOf(id) === -1){
this.following.push(id);
}
return this.save();
};
UserSchema.methods.unfollow = function(id){
this.following.remove(id);
return this.save();
};
UserSchema.methods.isFollowing = function(id){
return this.following.some(function(followId){
return id.toString() === followId.toString();
});
};

These are exactly the same as in Article favorite/unfavorite/isFavorite methods.

Let’s go to routes/api/profiles.js and add new routes which will use our newly created methods.

router.post('/:username/follow', auth.required, function(req,res,next){
User.findById(req.payload.id).then(function(user){
if(!user){return res.sendStatus(401);}
return user.follow(req.profile._id).then(function(){
return res.json({profile: req.profile.toProfileJSONFor(user)});
});
}).catch(next);
});
router.delete('/:username/follow', auth.required, function(req,res,next){
User.findById(req.payload.id).then(function(user){
return user.unfollow(req.profile._id).then(function(){
return res.json({profile: req.profile.toProfileJSONFor(user)});
});
}).catch(next);
});

Again, these routes are almost exactly the same as in Article’s for favoriting, so there is nothing new here.

Let’s go to models/User.js and do the last changes for following function to work:

UserSchema.methods.toProfileJSONFor = function(user){
return {
username: this.username,
bio: this.bio,
image: this.image,
/* THIS LINE IS EDITED */following: user ? user.isFollowing(this._id) : false
};
};

Edit he /*this line is edited*/ marked part only. Leave everything else unchanged.

Test the routes using Postman.

12. Tag route

In routes/api/ create a new file called tags.js:

var mongoose = require('mongoose');
var Article = mongoose.model('Article');
var router = require('express').Router();
router.get('/', function(req,res,next){
Article.find().distinct('tagList').then(function(tags){
return res.json({tags: tags});
}).catch(next);
});
module.exports = router;

A really simple file that is used to create a route which is going to display all the tags used in articles.

Don’t forget to add it to index.js in the same folder:

var router = require('express').Router();router.use('/', require('./users'));
router.use('/profiles', require('./profiles'));
router.use('/articles', require('./articles'));
/*new code*/router.use('/tags', require('./tags'));module.exports = router;

Don’t change anything except /*new code*/ line.

Test the route in Postman (GET localhost:8000/api/tags).

13. Querying articles

In /routes/api/articles.js we are going to add a new route:

router.get('/', auth.optional, function(req,res,next){
var limit = 20;
var offset = 0;
var query = {};
if(typeof req.query.limit !== 'undefined'){
limit = req.query.limit;
}
if(typeof req.query.offset !== 'undefined'){
offset = req.query.offset;
}
if(typeof req.query.tag !== 'undefined'){
query.tagList = {"$in": [req.query.tag]};
}
Promise.all([
req.query.author ? User.findOne({username: req.query.author}) : null,
req.query.favorited ? User.findOne({username: req.query.favorited}) : null
]).then(function(results){
var favoriter = results[1];
var author = results[0];
if(author){
query.author = author._id;
}
if(favoriter){
query._id = {$in: favoriter.favorites};
} else if(req.query.favorited){
query._id = {$in: []};
}
return Promise.all([
Article.find(query)
.limit(Number(limit))
.skip(Number(offset))
.sort({createdAt: 'desc'})
.populate('author')
.exec(),
Article.count(query).exec(),
req.payload ? User.findById(req.payload.id) : null
]).then(function(results){
var articles = results[0];
var articleCount = results[1];
var user = results[2];
return res.json({
articles: articles.map(function(article){
return article.toJSONFor(user);
}),
articleCount: articleCount
});
});
}).catch(next);
});

Don’t worry, this is the longest route you will see in this tutorial and it is not that complicated at all. We check if limit, offset or tag has been queried (…/articles?limit=10, …/articles?offset=2, …/articles?tag=new), and add them to query if they are.

We then look if favorited or author have been queried (…/articles?favorited=user1, …/articles?author=mark) and add them to query as well. Lastly we return an asyncpromise which limits by limit query we set, skips by offset query we set, populates the author, checks if user is logged in and executes. We then get results based on our queries (specific tags, limits, authors, favoriters etc.).

Test the route & queries (?x=y) in postman.

14. Feed (Last Step)

Again in /routes/api/articles.js add another route, but make sure it is the first route, above router.post(‘/’…):

router.get('/feed', auth.required, function(req,res,next){
var limit = 20;
var query = {};
var offset = 0;
if(typeof req.query.limit !== 'undefined'){
limit = req.query.limit;
}
if(typeof req.query.offset !== 'undefined'){
offset = req.query.offset;
}
User.findById(req.payload.id).then(function(user){
if(!user){return res.sendStatus(401);}
Promise.all([
Article.find({author: {$in: user.following}})
.limit(Number(limit))
.skip(Number(offset))
.populate('author')
.exec(),
Article.count({author: {$in: user.following}})
]).then(function(results){
var articles = results[0];
var articleCount = results[1];
return res.json({
articles: articles.map(function(article){
return article.toJSONFor(user);
}),
articleCount: articleCount
});
});
}).catch(next);
});

A route almost exactly like the last one but it does not display all articles, but only of the authors who an user follows.

Test the route in Postman, make sure to follow someone who has an article first!

Backend completed!

Congratulations! You have finished my REST API Backend using Express.js tutorial! I hope you learned something and my instructions weren’t too terrible. Thank you for your time, and just FYI Frontend part on this exact backend is coming soon. :)

Antonio Erdeljac

--

--

Antonio Erdeljac
Antonio Erdeljac

Responses (5)