I Created A Dev.to Clone With Nodejs, Express & MongoDB.
Tags: express, javascript, api, blog
Published: 2023-01-08 22:42:50


**Introduction**
In this article I will be showing you how to create a Dev.to clone with Nodejs, Express, MongoDB, with JWT authentication. This API will give ability to users to create, read, update and delete blog alongside adding an authentication layer to it. 

**Pre-requisite**

1. NODEJS- You Should have node installed.
2. MongoDB - You Should have mongoDB installed or have an account if you prefer to use the cloud version. 

**Packages that will be required**
You can install the following using `npm` or its equivalent 

```
 "dependencies": {
    "bcryptjs": "^2.4.3",
    "cookie-parser": "^1.4.6",
    "cors": "^2.8.5",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "express-mongo-sanitize": "^2.2.0",
    "express-rate-limit": "^6.6.0",
    "helmet": "^6.0.0",
    "hpp": "^0.2.3",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^6.7.0",
    "morgan": "^1.10.0",
    "morgan-json": "^1.1.0",
    "nodemailer": "^6.8.0",
    "nodemon": "^2.0.20",
    "slugify": "^1.6.5",
    "validator": "^13.7.0",
    "winston": "^3.8.2",
    "xss-clean": "^0.1.1"
  }
```
**Step 1 - File Structure**
Before we write a line of code, lets create a `package.json` file by running `npm init -y` in our terminal. 
Now we would be using a file structure like the one below, which follows the MVC architecture. 
 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ku4lw6cjjwo5575qpllo.PNG)

**Step 2 - Lets create our environmental variables in our `.env` file**

```
NODE_ENV=Developemt
PORT=8000
DATABASE_LOCAL=mongodb://127.0.0.1:27017/blogDB
DATABASE_PROD=mongodb+srv://blak:<password>@cluster0.1nb5crb.mongodb.net/blogdb?retryWrites=true&w=majority
DATABASE_PASSWORD=l3Ax3f4CaoJrKtjj
jwt_secret=mytokensecretblogapp20221026
jwt_expires=1h
jwt_cookie_expires=2
```
Notice I have two database URL, One for the local DB and the other for the cloud DB, You can pick your preference. 

**Step 3 - Lets write some code in `server.js` to start up our server and help us connect to our Database**

```
const mongoose = require('mongoose');
const app = require('./app');
const dotenv = require('dotenv');
dotenv.config({ path: './config.env' });

//CREATE DB CONNECTION
let DB = process.env.DATABASE_PROD.replace(
  '<password>',
  process.env.DATABASE_PASSWORD
);

if (process.env.NODE_ENV == 'Development') {
  DB = process.env.DATABASE_LOCAL;
}
mongoose
  .connect(DB, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then((con) => {
    console.log('DB Connection Successful');
  });

//Connect To Server
const port = process.env.PORT || 8080;
const server = app.listen(port, () => {
  console.log(`App running on port ${port}`);
});
```

**Step 4 - Lets write some code in our `app.js` file**
This file contains code where all our middlewares will be registered, middlewares such as our route middlewares, global error handler middleware, some third-party middlewares for security and rate limiting 

```
const express = require('express');
const app = express();
const UserRouter = require('./routes/userRoute');
const blogRouter = require('./routes/blogRoute');
const appError = require('./utils/appError');
const cookieParser = require('cookie-parser');
const globalErrHandler = require('./controllers/errorController');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
const hpp = require('hpp');
const cors = require('cors');
const morganMiddleware = require('./utils/morgan');
const logger = require('./utils/logger');

//GLOBAL MIDDLEWARES
//Allow cross-origin access
app.use(cors());

//Set security HTTP headers
app.use(helmet());

const limiter = rateLimit({
  max: 500,
  windowMs: 24 * 60 * 60 * 1000,
  standardHeaders: true,
  message: 'Too Many Request From this IP, please try again in an hour',
});

//Set API Limit
app.use('/api', limiter);

//Data Sanitization against NOSQL query Injection
app.use(mongoSanitize());

//Data Sanitization against XSS
app.use(xss());

//Morgan Middleware
app.use(morganMiddleware);
//Allow views
app.use(express.static(`${__dirname}/views`));
app.use((req, res, next) => {
  req.requestTime = new Date().toISOString();
  next();
});

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use('/api/v1/users', UserRouter);
app.use('/api/v1/articles', blogRouter);

//Global Error Handler
app.all('*', (req, res, next) => {
  return next(
    new appError(404, `${req.originalUrl} cannot be found in this application`)
  );
});

app.use(globalErrHandler);
module.exports = app;

```
**Step 5 - Lets create our data models now.**
Now that our server is up and connected to our DB, lets start modelling our data. we would start with user model, so lets head into the models folder and create a file called `userModel.js` and paste the code below into it. 

```
const mongoose = require('mongoose');
const validator = require('validator');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');

const userSchema = new mongoose.Schema(
  {
    firstname: {
      type: String,
      required: [true, 'Please provide firstname'],
    },
    lastname: {
      type: String,
      required: [true, 'Please provide lastname'],
    },
    email: {
      type: String,
      required: [true, 'Please Provide Email Address'],
      unique: true,
      validate: [validator.isEmail, 'Please a valid email address'],
    },
    username: {
      type: String,
      unique: true,
    },
    password: {
      type: String,
      required: [true, 'Please Provide A Password'],
      minlength: 8,
      select: false,
    },
    passwordConfirm: {
      type: String,
      required: [true, 'Please Fill Password Field'],
      validate: function (el) {
        return el === this.password;
      },
      message: 'Password do not match.',
    },
    resetPasswordToken: String,
    resetTokenExpires: Date,
  },
  {
    toJSON: { virtuals: true },
    toObject: { virtuals: true },
  }
);
userSchema.pre('save', async function (next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 12);
  this.passwordConfirm = undefined;
  this.username = this.username.toLowerCase();
  next();
});
userSchema.methods.createResetToken = function () {
  const resetToken = crypto.randomBytes(32).toString('hex');
  this.resetPasswordToken = crypto
    .createHash('sha256')
    .update(resetToken)
    .digest('hex');
  console.log({ resetToken }, this.resetPasswordToken);
  this.resetTokenExpires = Date.now() + 30 * 60 * 1000;
  return resetToken;
};
userSchema.methods.comparePassword = async function (
  signinPassword,
  userPassword
) {
  return await bcrypt.compare(signinPassword, userPassword);
};
//Virtual Properties To Load articles per user
userSchema.virtual('articles', {
  ref: 'Blog',
  foreignField: 'author',
  localField: '_id',
});
const User = mongoose.model('User', userSchema);

module.exports = User;

```
In this file we create our model using mongoose and also used some mongoose hooks, methods & virtual properties  to:

1. Hash our users password before saving to the DB using `bcrypt.js` package for hashing & unhashing.
2. Create a reset token, when a user forgets his/her password.
3. Add a reference to each blog created by a user. 

**Step 6 - Lets create the signup and login functionality**
Head to the controller folder and create a file called `authController.js`, and copy the following code 

```
const jwt = require('jsonwebtoken');
const catchAsy = require('./../utils/catchAsync');
const appError = require('./../utils/appError');
const User = require('./../models/userModel');
const sendEmail = require('./../utils/email');
const crypto = require('crypto');
const { promisify } = require('util');
const app = require('../app');
const Logger = require('../utils/logger');
const signToken = (id) => {
  return jwt.sign({ id }, process.env.jwt_secret, {
    expiresIn: process.env.jwt_expires,
  });
};

const createToken = (user, statusCode, res) => {
  const token = signToken(user._id);
  const cookieOptions = {
    expires: new Date(
      Date.now() + process.env.jwt_cookie_expires * 60 * 60 * 1000
    ),
    httpOnly: true,
  };
  if (process.env.NODE_ENV === 'production') cookieOptions.secure = true;

  //Send Token To Client
  res.cookie('jwt', token, cookieOptions);

  //Remove Password From JSON
  user.password = undefined;

  //Send Response To Client
  res.status(statusCode).json({
    status: 'Success',
    data: {
      token,
      user,
    },
  });
};
exports.signup = catchAsy(async (req, res, next) => {
  const user = await User.create(req.body);

  createToken(user, 200, res);
});
exports.login = catchAsy(async (req, res, next) => {
  const { email, password } = req.body;

  //If user does not provide any of the required fields
  if (!email || !password) {
    return next(new appError(401, 'Please Provide Email or Password.'));
  }

  const user = await User.findOne({ email }).select('+password');

  //If Any Field Is Incorrect
  if (!user || !(await user.comparePassword(password, user.password))) {
    const message = 'Email or Password Incorrect';
    return next(new appError(401, message));
  }

  //If everything checks out, send JWT Token.
  Logger.info(`New User Logged in with the ID of ${user.id}`);
  createToken(user, 200, res);
});

exports.forgetPassword = catchAsy(async (req, res, next) => {
  //1.Get User From Posted Email
  const user = await User.findOne({ email: req.body.email });

  if (!user) return next(new appError(404, 'Email Address Not Found!'));

  //2. Create Token and Save to DB
  const resetToken = user.createResetToken();
  await user.save({ validateBeforeSave: false });

  //3. Send to Client
  const resetUrl = `${req.protocol}://${req.get(
    'host'
  )}/api/v1/users/resetpassword/${resetToken}`;
  const message = `You made a request for a password reset, Click on the link to reset your password, reset token is valid for 30mins! ${resetUrl} \n Please Ignore if you did not make this request`;

  try {
    sendEmail({
      email: user.email,
      subject: `Your Password Reset Token(Valid 30min)`,
      message,
    });
    res.status(201).json({
      status: 'Success',
      message: 'Please check inbox for reset token!',
    });
  } catch (err) {
    user.resetPasswordToken = undefined;
    user.resetTokenExpires = undefined;
    return next(
      new appError(
        500,
        'There was an error sending mail, Please try again later!'
      )
    );
  }
});

exports.resetPassword = catchAsy(async (req, res, next) => {
  //1. Grab token from resetUrl
  const hashedToken = crypto
    .createHash('sha256')
    .update(req.params.token)
    .digest('hex');

  const user = await User.findOne({
    resetPasswordToken: hashedToken,
    resetTokenExpires: { $gt: Date.now() },
  });
  //2. Check IF token matches & still valid
  if (!user)
    return next(
      new appError(
        400,
        'Token Invalid or Expired, Try to reset password again!'
      )
    );
  //3. IF Token is valid
  user.password = req.body.password;
  user.passwordConfirm = req.body.passwordConfirm;
  user.resetPasswordToken = undefined;
  user.resetTokenExpires = undefined;
  await user.save();
  //4. Login User
  createToken(user, 200, res);
});

exports.protectRoute = catchAsy(async (req, res, next) => {
  const token = req.cookies.jwt;
  if (!token) return next(new appError(400, 'Please Login Again!'));

  const decoded = await promisify(jwt.verify)(token, process.env.jwt_secret);
  //Check if user exists
  const currentUser = await User.findById(decoded.id);
  //if (decoded.expiresIn > Date.now() + jwt_cookie_expires * 60 * 60 * 1000)
  if (!currentUser)
    return next(new appError(404, 'Session expired, Login again!'));
  //Add user to req object
  req.user = currentUser;
  next();
});

```
Here we allow users to register and login and also issue a JWT after a successful login. 

**Step 7 - Lets mount our routes**
Head to the route folder and create a file called `userRoute.js`, copy the following code.

```
const express = require('express');
const authController = require('./../controllers/authController');
const router = express.Router();

router.post('/signup', authController.signup);
router.post('/login', authController.login);
router.post('/forgotPassword', authController.forgetPassword);
router.patch('/resetpassword/:token', authController.resetPassword);

module.exports = router;

```
lets test what we have now but lets configure some scripts in `package.json` file.

```
"scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
```
Now we can go ahead to test our project now by using `npm start` in our terminal and if you wrote the code correctly users should be able to create an account and login now.

**Step 8 - Lets create our blog model**
Now we are done with the users end, lets work on creating the model for the blog itself. Head to the models folder and create another file called `blogModel.js`, copy the following code. 

```
const mongoose = require('mongoose');
const validator = require('validator');
const slugify = require('slugify');

const blogSchema = new mongoose.Schema(
  {
    title: {
      type: String,
      required: [true, 'Your Blog Requires A Title'],
      trim: true,
      unique: true,
    },
    slug: {
      type: String,
    },
    description: {
      type: String,
      required: [true, 'Please add a description'],
      trim: true,
    },
    author: {
      type: mongoose.Schema.ObjectId,
      ref: 'User',
    },
    state: {
      type: String,
      enum: ['draft', 'published'],
      default: 'draft',
    },
    read_count: {
      type: Number,
      default: 0,
    },
    reading_time: {
      type: String,
      default: 0,
    },
    tags: {
      type: [String],
    },
    body: {
      type: String,
      required: [true, 'Your blog must have a body!'],
    },
  },
  {
    toJSON: { virtuals: true },
    toObject: { virtuals: true },
  }
);
blogSchema.set('timestamps', true);
blogSchema.pre(/^find/, function (next) {
  this.populate({
    path: 'author',
    select: '-__v -resetPasswordToken -resetTokenExpires -password -email',
  });
  next();
});
blogSchema.pre('save', function (next) {
  this.slug = slugify(this.title, { lower: true });
  this.tags.forEach((el) => {
    el.toLowerCase();
  });
  next();
});
blogSchema.methods.updateRead = async function () {
  this.read_count = this.read_count + 1;
  return this.read_count;
};
blogSchema.methods.calcReadTime = async function () {
  let words = this.title.length + this.body.length;
  let time = words / 200;
  const fullTime = time.toString().split('.');
  const min = fullTime[0];
  const sec = Math.round((fullTime[1] * 60) / 1000);
  return [min, sec];
};
const Blog = mongoose.model('Blog', blogSchema);

module.exports = Blog;

```
In this file we create our model using mongoose and also used some mongoose hooks, methods to:

1. Slugify blog titles. 
2. Calculate the read-time of each blog post.
3. Update the read count of each blog. 
4. Add a reference to the author who created the blog. 

**Step 9 - Lets create functionalities for the blog**
Head to the controller folder and create a file called `blogController.js`, and copy the following code 

```
const Blog = require('./../models/blogModel');
const User = require('./../models/userModel');
const catchAsy = require('./../utils/catchAsync');
const appError = require('./../utils/appError');
const slugify = require('slugify');

exports.createBlog = catchAsy(async (req, res, next) => {
  req.body.author = req.user._id;
  const blog = await Blog.create(req.body);
  const readTime = await blog.calcReadTime();
  blog.reading_time = `${readTime[0]} min ${readTime[1]} seconds`;
  await blog.save({ validateBeforeSave: false });

  res.status(200).json({
    status: 'Success',
    data: {
      blog,
    },
  });
});

exports.getAllBlogs = catchAsy(async (req, res, next) => {
  //1. Create a query
  let query = Blog.find();

  //2. Check if user queries for any blog in draft state.
  if (req.query.state == 'draft') {
    return next(new appError(403, 'You cannot access unpublished articles!'));
  } else {
    query = Blog.find(req.query);
  }

  //Build query for author
  if (req.query.author) {
    const author = req.query.author;
    const user = await User.findOne({ username: author });
    if (!user)
      return next(
        new appError(403, 'Author does not exists or has written no articles')
      );
    const ID = user.id;
    query = Blog.find({ author: ID });
  }

  //Build query for tags
  if (req.query.tag) {
    const tag = req.query.tag.split(',');
    query = Blog.find({ tags: tag });
  }

  //Build Query For sort
  if (req.query.sort) {
    const sort = req.query.sort || 'createdAt';
    query = query.sort(sort);
  }
  //.Add Pagination
  const page = req.query.page * 1 || 1;
  const limit = req.query.limit * 1 || 20;
  const skip = (page - 1) * limit;
  query = query.skip(skip).limit(limit);

  //Await Query, and filter drafts out.
  const blog = await query;
  let newblog = [];
  if (blog.length == 0) return next(new appError(403, 'No Blog Found'));
  blog.forEach((el) => {
    if (el.state == 'published') {
      newblog.push(el);
    }
  });

  res.status(200).json({
    status: 'success',
    result: newblog.length,
    data: {
      newblog,
    },
  });
});

exports.getBlog = catchAsy(async (req, res, next) => {
  const ID = req.params.id;
  const blog = await Blog.findById(ID);

  if (blog.state == 'draft') {
    return next(new appError(403, 'You cannot access unpublished blog'));
  }
  const count = blog.updateRead();
  await blog.save({ validateBeforeSave: false });
  res.status(200).json({
    status: 'Success',
    data: {
      blog,
    },
  });
});

exports.updateBlog = catchAsy(async (req, res, next) => {
  //1. Get Blog Id and Perform search in DB
  const blogID = req.params.id;
  const blog = await Blog.findById(blogID);

  //2. Return error when blog cannot be found
  if (!blog) return next(new appError(404, 'No Blog Found'));

  //3. Check if user is owner of blog
  if (blog.author.id === req.user.id) {
    //4. If everything checks out, allow user edit blog.
    const newBlog = await Blog.findByIdAndUpdate(blogID, req.body, {
      new: true,
      runValidators: true,
    });
    //5. Return data to user
    res.status(200).json({
      status: 'success',
      data: {
        newBlog,
      },
    });
  } else {
    return next(new appError(403, 'Action Forbidden, You cannot Update blog'));
  }
});

exports.deleteBlog = catchAsy(async (req, res, next) => {
  //1. Get Blog Id and Perform search in DB
  const blogID = req.params.id;
  const blog = await Blog.findById(blogID);

  //2. Return error when blog cannot be found
  if (!blog) return next(new appError(404, 'No Blog Found'));

  //3. Check if user is owner of blog
  if (blog.author.id === req.user.id) {
    //4. If everything checks out, allow user delete blog.
    const newBlog = await Blog.findByIdAndDelete(blogID);
    //5. Return data to user
    res.status(204).json({
      status: 'success',
      data: null,
    });
  } else {
    return next(new appError(403, 'Action Forbidden, You cannot delete blog'));
  }
});

exports.myBlog = catchAsy(async (req, res, next) => {
  const queryObj = { ...req.query };
  const excludedFields = ['page', 'sort', 'limit', 'fields'];
  excludedFields.forEach((el) => delete queryObj[el]);

  //1. Grab user ID from protect route
  const userID = req.user.id;

  //2. Use ID To find Blog where it matches the author ID.
  let query = Blog.find({ author: userID });

  //3. Build Query For Other Query
  if (req.query.state) {
    const state = req.query.state;
    query = Blog.find({ author: userID, state: state });
  }
  //4.Add Pagination
  const page = req.query.page * 1 || 1;
  const limit = req.query.limit * 1 || 5;
  const skip = (page - 1) * limit;
  query = query.skip(skip).limit(limit);
  const blog = await query;

  res.status(200).json({
    status: 'success',
    result: blog.length,
    data: {
      blog,
    },
  });
});

```
Here we configure all the functionalities of our blog _go through the comments in the code body_.

*Step 10 - Lets mount our routes for the blog**
Head to the route folder and create a file called `blogRoute.js`, copy the following code.

```
const express = require('express');
const blogController = require('./../controllers/blogController');
const authController = require('./../controllers/authController');

const router = express.Router();
router
  .route('/myblogs')
  .get(authController.protectRoute, blogController.myBlog);

router
  .route('/')
  .post(authController.protectRoute, blogController.createBlog)
  .get(blogController.getAllBlogs);

router.route('/:id').get(blogController.getBlog);

router
  .route('/update/:id')
  .patch(authController.protectRoute, blogController.updateBlog);

router
  .route('/delete/:id')
  .delete(authController.protectRoute, blogController.deleteBlog);
module.exports = router;

```
Now the major functionalities of our application are done, lets just add some utilities to make our app perfect.

**Error Class Util**
Head to the util folder and create a file called `appError.js`, copy the following code.

```
class appError extends Error {
  constructor(statusCode, message) {
    super(message);

    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'failed' : 'Server Error';
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

module.exports = appError;

```
To make this appError util work we need a corresponding controller file, so lets create a new controller called `errorController.js` and copy the following code.

```
const appError = require('./../utils/appError');

const handleCastErrorDB = (err) => {
  const message = `Invalid ${err.path}: ${err.value}`;
  return new appError(400, message);
};
const handleDuplicateKeyDB = (err) => {
  const value = err.keyValue.email;
  const message = `Duplicate field value: '${value}' Please use another value`;
  return new appError(400, message);
};
const handleValidationErrorDB = (err) => {
  const errors = Object.values(err.errors).map((el) => el.message);
  const message = `Invalid field data ${errors.join('. ')} Critical Error`;
  return new appError(400, message);
};
const handleJWTError = (err) =>
  new appError(401, 'Invalid token. Please login again');
const handleJWTExpiredError = (err) =>
  new appError(401, 'Your token is expired, Please login again');

const errorDev = (err, res) => {
  res.status(err.statusCode).json({
    status: err.status,
    message: err.message,
  });
};
const errorProd = (err, res) => {
  if (err.isOperational) {
    res.status(err.statusCode).json({
      status: err.status,
      message: err.message,
    });
    //Programming Errors or other unknown Error: Don't leak error details
  } else {
    console.error('ERROR 💣', err);
    res.status(500).json({
      status: 'error',
      message: 'Something went very wrong',
    });
  }
};

module.exports = (err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'Server Error';
  if (process.env.NODE_ENV == 'Development') {
    console.log(err);
    errorDev(err, res);
  } else if (process.env.NODE_ENV == 'production') {
    let error = { ...err };
    error.message = err.message;
    if (error.name === 'CastError') error = handleCastErrorDB(error);
    if (error.code === 11000) error = handleDuplicateKeyDB(error);
    if (error._message === 'Validation failed')
      error = handleValidationErrorDB(error);
    if (error.name === 'JsonWebTokenError') error = handleJWTError(error);
    if (error.name === 'TokenExpiredError')
      error = handleJWTExpiredError(error);
    errorProd(error, res);
    console.log(err);
  }
};
```
Now we have successfully setup a robust way of handling both development and production error. 

**Catch async Util**
Head to the util folder and create a file called `catchAsync.js`, copy the following code.

```
module.exports = (fn) => {
    return (req, res, next) => {
        fn(req, res, next).catch(next);
    }
}
```
This code block is to handle the repeated use of try and catch block, thereby making our code more readable.

**Email Service Util**
Head to the util folder and create a file called `email.js`, copy the following code.

```
const nodemailer = require('nodemailer')

const sendEmail = async (options) => {
    const transporter = nodemailer.createTransport({
        host: process.env.EMAIL_HOST,
        port: process.env.EMAIL_PORT,
        auth: {
          user: process.env.EMAIL_USERNAME,
          pass: process.env.EMAIL_PASSWORD,
        },
    })

    const mailOptions = {
        from: 'Kelechi Okoronkwo <hello@blakcoder.tech>',
        to: options.email,
        subject: options.subject,
        text: options.message
    }
    await transporter.sendMail(mailOptions)
}
module.exports = sendEmail
```
This util is to help in sending emails to users when requested, for example when users request for a password reset. Finally to make our email service work we would need to update our ` .env` file with the following code.

```
EMAIL_USERNAME='4602f30010ad95'
EMAIL_PASSWORD='1f4568221277a4'
EMAIL_HOST='smtp.mailtrap.io'
EMAIL_PORT=2525

```
I made use of _mailtrap_ for email testing, you can get the following details by creating an account on [mailtrap.io](mailtrap.io).

**Finally you can test and deploy to your favorite platform**

Here is the complete source code [Source Code](https://github.com/anslemkelechi/Blog-API)

Cheers
May the force be with you.