My Altschool Blog Project Documentation

My Altschool Blog Project Documentation

Altschool Africa is an institution that takes a non-traditional approach to learning by teaching courses directly connected with a selected track.

I am a student of Altschool Africa enrolled in their school of engineering with the main focus of Backend engineering using javascript. We were given an assignment to practice the skills we had learned so far, the task was to build a blogging API with the following functionalities:

  1. Users should have their first name, last name, email, and password.

  2. The user should be able to sign up and sign in to the blog app

  3. Use JWT as an authentication strategy and expire the token after 1 hour

  4. A blog can be in two states: draft and published

  5. Logged-in and not logged in users should be able to get a list of published blogs created

  6. Logged-in and not logged in users should be able to get a published blog

  7. Logged-in users should be able to create a blog.

  8. When a blog is created, it is in a draft state

  9. The owner of the blog should be able to update the state of the blog to publish

  10. The owner of the blog should be able to edit the blog in a draft or published state

  11. The owner of the blog should be able to delete the blog in a draft or published state

  12. The owner of the blog should be able to get a list of their blogs.

  13. The endpoint should be paginated and filterable by state

  14. When a single blog is requested, the API should return the user information(the author) with the blog. The read_count of the blog too should be updated by 1.

After successfully building the API we were told to document the process, which is what I will be doing in this article.

Requirements

To understand or follow along with this article, you will need:

  • Knowledge of JavaScript

  • Familiarity with nodejs

  • An IDE such as VScode

  • MongoDB either local or on the cloud

Setting up a basic server

I started by creating a basic server using express and nodemon with the following steps:

Navigate to the folder where you want to build the app and run npm init -y in the terminal to create the package.json file, the -y flag is to accept all default options.

Install the required packages for the basic server by running npm install express nodemon in the terminal, express is for creating the server and nodemon will be used to monitor the codebase for any changes.

Create an index.js file and input the following code:

// Require express module
const express = require('express');

// Create an app using the express module
const app = express();

// Add a simple endpoint
app.get('/', (req, res) => {
    res.send("Hello World")
})

// Listen for requests made to the server
app.listen(4000, (req, res) => {
    console.log(`Server is running on port: 4000`)
})
  • Run nodemon index.js in the terminal, using nodemon automatically restarts the server whenever any changes are made to it. Open your browser and go to localhost:4000, you should see the text "Hello World", this means the server is functioning properly.

MVC Pattern

For this project I made use of the MVC architecture, this is a system design practice that divides the codebase into three parts which contain code for distinct activities. MVC stands for Models, Views, and Controllers, which contain code for the Database, User Interface, and Business Logic respectively.

Models

Models are used to interact with the database, they help to design the structure and format of database objects by creating schemas. I created one for both the users and the articles.

User Model

The user model contains all the required fields:

  • first name

  • last name

  • email

  • password

  • articles

Create a Models folder and create a file called userModel.js inside it, then input the following code:

// Require the mongoose package
const mongoose = require('mongoose');

// Instantiate the schema class from the mongoose package
const Schema = mongoose.Schema;

// Create a user schema with the mongoose schema
const UserSchema = new Schema({
    firstName: {
        type: String,
        required: [true, "Please enter your first name"]
    },
    lastName: {
        type: String,
        required: [true, "Please enter your last name"]
    },
    email: {
        type: String,
        required: [true, "Please enter an email"],
        unique: [true, "This email is already registered, sign in!"],
        lowercase: true,
    },
    password: {
        type: String,
        required: true,
        minLength: [5, "Password must be at least 5 characters"]
    },
    articles: {
        type: Array,
    }
}, { timestamps: true });

// This is a function for logging a user in

// more details in the  userController section
UserSchema.statics.login = async function(email, password) {
    const user = await this.findOne({ email });
    if(!user){
        throw Error('Incorect email, try again or sign up!');
    }

    const auth = await bcrypt.compare(password, user.password);
    if(!auth){
        throw Error('Incorrect password');
    }

    return user
}

// Create a user model with the user schema
const User = mongoose.model('users', UserSchema);

// Export the user Schema
module.exports = User;

The texts in the square brackets are error messages which are thrown in case certain validation is not met when filling in the fields. The timestamps: true is used to automatically generate timestamps at the points where the object is created or updated. The login function is a static function created in the user model this makes it more secure and less prone to tampering.

Article Model

For the article model, I created the following fields

  • title

  • description

  • author

  • state

  • read_count

  • read_time

  • tags

  • body

Create a file called articleModels in the Models folder and input the following code:

// Require the mongoose package
const mongoose = require('mongoose');

// Instantiate the schema class from the mongoose package
const Schema = mongoose.Schema;

// Create an article schema with the mongoose schema
const articleSchema = new Schema({
    title: {
      type: String,
      required: [true, "Please provide the title"],
      unique: [true, "The title name already exists"],
    },
    description: {
      type: String,
    },
    author: {
      type: String,
      required: [true, "Please provide the author"],
    },
    state: {
      type: String,
      enum: ["draft", "published"],
      default: "draft",
    },
    read_count: {
      type: Number,
      default: 0,
    },
    reading_time: {
      type: String,
      required: [true, "Please provide the reading time"],
    },
    tags: {
      type: String,
      required: [true, "Please provide the tags"],
    },
    body: {
      type: String,
      required: [true, "Please provide the body"],
    }
}, {timestamps: true});

// Create an article model with the user schema
const Article = mongoose.model('articles', articleSchema);

// Export the article model
module.exports = Article;

When an article is created its state is set to "draft" by default, this can later be updated to "published" by the user. The read_count property of the blog is also initially set to zero, other features like timestamps and id are auto-generated.

Routes

Routes are also another essential part of the application, as they connect the endpoints to the controllers that carry out the blog functions. Adding all the routes to the main page can make things a little overcrowded, hence I created a "routes" folder that would contain the necessary routes.

User routes

The userRoutes file will contain all the routes for all user-related functions such as:

  • /signup => For registering a user into the database

  • /login => For logging in an existing user

  • /logout => For logging out a user

When a certain endpoint is called it redirects the request to a controller which handles the request. The "userRoutes" file will contain the following code:

const express = require('express')
const userRouter = express.Router();

userRouter.post('/signup', () => {
  // Function for signing a new user in goes here
  res.send('Signs in a new user')
});

userRouter.post('/login', () => {
  // Function for logging a user in goes here
  res.send('Logs in an existing user')
});

userRouter.post('/logout', () => {
  // Function for logging out a user goes here
  res.send('Signs out a user')
});

module.exports = userRouter;

The above code creates a router, userRouter using the express.router() method the userRouter will be connected to all the required endpoints and functions which will then be exported to the app.js file where it will be used by the server.

The routes above were tested using postman, the messages in the res.send help to identify that the routes are working properly.

Notice how all the endpoints are POST requests, this is because all user functions send data to the server.

Article routes

The articleRoutes file contains endpoints for all the article-related requests, it contains the following code:

const express = require('express')
const blogRouter = express.Router();

blogRouter.get('/', () => {
  // Function for getting all available blogs
  res.send('Gets all available articles')
});

blogRouter.get('/user', () => {
  // Function for getting all the articles by a user
  res.send('Gets all articles by a user')
});

blogRouter.get('/:id', () => {
  // Function for getting an article by id
  res.send('Gets an article by id')
});

blogRouter.post('/', () => {
  // Function for creating a new article
  res.send('Creates a new article')
});

blogRouter.delete('/:id', () => {
  // Function for updating an article
  res.send('Updates an article')
});

blogRouter.patch('/:id', () => {
  // Function for deleting an article
  res.send('Deletes an article')
});

module.exports = blogRouter;

In this file, each endpoint signifies a single request which will be handled in the controller part. The texts in the brackets are simple placeholder messages to ensure that the endpoints are working properly when tested with postman.

The next step is to connect these routes to the server by updating the code in the index.js file to look like this:

...

// Require the routers
const userRouter = require('./routes/userRoutes')
const articleRouter = require('./routes/articleRoutes')

...

// Using the routers
app.use('/api/v1/user', userRouter)
app.use('/api/v1/blog', articleRouter)

...

The routers must come after the instantiation of the app, and they will be mapped using specific endpoints, for instance, a request to the api/v1/user/<something> would first be transferred to the userRouter in the userRoutes file.

Controllers

The controllers contain the code for processing and handling requests, interacting with the database with the models, and sending responses back to the user.

Create a controllers folder that will contain the two controllers, the controllers will be connected to the server by linking them with their respective route files.

User Controller

The userController will contain functions that the sign-up, sign-in, and sign-out requests. The file contains the following code:

const User = require('../models/userModel');
const bcrypt = require('bcrypt')

The User object is required from the userModel and it will be used to interact with the database. The bcrypt library is used for hashing and decrypting passwords, it will be used in the login and logout sections.

// Signup function
const signup = async (req, res) => {

  // Checks if user already exists
  const user = await User.findOne({ email: req.body.email })
  if(user){
      console.log("This user already exists, you have been logged in!")
      // Redirect them to log in page
      return res.redirect('/api/v1/user/login')
  }

  try{
      // Creates new user and hashes the password
      const user = new User(req.body);
      const salt = await bcrypt.genSalt(10);
      user.password = await bcrypt.hash(user.password, salt);
      await user.save();

      // Returns the user data
      return res.status(201).json({
          status: "success",
          message: "Sign up successful!, you have been automatically logged in",
          data: {
              firstName: user.firstName,
              lastName: user.lastName,
              email: user.email,
              articles: user.articles,
              id: user._id
          }
      })
  }
  // Throws error if any
  catch(err){
      res.status(400).send(err.message)
  }
}

When a new user is created via sign up, information like the name, email, and password, is taken from the request body and used to create a new user in the database. Once the user has been created successfully, the details are sent back as a response to the user.

// Login function
const login = async (req, res) => {

    // Obtains email and password from request body
    const { email, password } = req.body;

    try{
      // Uses login static function
        const user = await User.login(email, password);

        // Returns user data
        res.status(201).json({
            status: "success",
            message: "You logged in successfully",
            data: {
                firstName: user.firstName,
                lastName: user.lastName,
                email: user.email,
                articles: user.articles,
                id: user._id
            }
        });
    }
    // Throws error if any
    catch(err){
        res.status(400).send(err.message)
    }
}

The login function takes two parameters from the request body: "email" and "password". These details are used to log the data into the database using the static login function in the userModel file, but this only works if the user is already registered in the database.

Note that the user password is not sent as part of the response, this is a basic security measure.

const logout = (req, res) => {
    // Implement log-out function later
    res.send('Logged out successfully')
}

// Exports all the functions
module.exports = {
    signup,
    login,
    logout
}

The logout function will be implemented later as it has to do with clearing the JWT tokens, this will be discussed in the JWT section.

All three functions for the user controller are then exported so they can be required in the routes folder where they will be connected to their respective endpoints.

Blog controller

The blog controller file will contain all the code for handling all requests made to the blog object. The following code will go into this file:

// Require the article model
const Article = require('../models/articleModel');

// External functions required
const { checkUser } = require('../middleware/jwt');
const { readingTime } = require('../middleware/utils')

The readingTime function is used to calculate the time it will take to read an article using the average reading speed of 180 words/minute. The checkUser function is used to check the current logged-in user by decoding the JWT token and using the data to search for the user in the database.

JWT tokens will be discussed in the JWT authentication section.

const getAllArticles = async (req, res) => {
    // Get pagination details from the query
    const limit = parseInt(req.query.limit)
    const offset = parseInt(req.query.skip)

    // Get all published articles from database
    const articles = await Article.find()
        .where({ state: "published"})
        .skip(offset)
        .limit(limit)

    // Error message, if there are no published blogs
    if(!articles.length){
        return res.json({
            status: "failed",
            message: "There are no published articles, check Drafts!"
        })
    }
    // Apply pagination
    const articleCount = articles.length
    const currentPage = Math.ceil(articleCount % offset)
    const totalPages = Math.ceil(articleCount / limit)

    // Return published articles
    res.status(200).json({
        status: "success",
        message: "All published articles",
        total: articleCount,
        page: currentPage,
        pages: totalPages,
        data: articles
    })

}

The getAllArticles function returns all the existing articles in the database. Pagination is implemented here as all the articles can be very large and it will be bad practice to return everything at once. All the articles will be returned page by page and the parameters for the pagination are passed in the URL, e.g https://localhost/api/v1/blog/?limit=2&offset=5. The limit and offset properties are query parameters that will be parsed into the URL.

const getMyArticles = async (req, res) => {
    // Get pagination details
    const limit = parseInt(req.query.limit)
    const offset = (req.query.limit)

    // Check for the current user
    const user = await checkUser(req, res)

    // Get data from database
    const userArticles = Article.find({ author: user.firstName})
        .skip(offset)
        .limit(limit)

    // Throw error message if there are no blogs
    if(!userArticles.length){
        return res.json({
            status: "failed",
            message: "This user does not have any published articles"
        })
    }

    // Apply pagination
    const articleCount = userArticles.length
    const totalPages = Math.ceil(articleCount / limit)
    const currentPage = Math.ceil(articleCount % offset)

    // Return article data
    res.status(200).json({
        status: "success",
        message: `All articles, published by ${user.firstName}`,
        total: articleCount,
        page: currentPage,
        pages: totalPages,
        data: userArticles
    })
}

The getMyArticles function returns all the articles by a particular user, by checking through the database for articles where the author corresponds with the name of the logged-in user, the data is then paginated and sent to the user.

const getArticle = async (req, res) => {
    // Get article from database with Id
    const article = await Article.findById(req.params.id)
        .where({ state: "published" })

    // Throw error message if article is not found
    if(!article){
        return res.status(404).send("The Article you requested was not found")
    }

    // Increase read count
    article.read_count++
    article.save()

    // Return data
    res.status(200).json({
        status: "success",
        message: `Single article post: "${article.title}"`,
        data: {
            article
        }
    })
}

The getArticle function gets a single article by searching through the database with the id that is passed in the request parameter. It also increases the read_count property every time a blog is requested.

const createArticle = async (req, res) => {

    try{
        // Get details from the request body
        const { title, description, state, tags, body } = req.body;

        // Check if article with that title exists
        const exists = await Article.findOne({ title })
        if(exists){
            return res.json({
                status: "failed",
                message: "Article with that title already exists"
            })
        }

        // Check for the current user
        const user = await checkUser(req, res)

        if(!user){
            return res.json({
                status: "failed",
                message: "You need to be logged in to create a articlepost"
            })
        }

        // Name of user is set to author of article
        const article = new Article({
            title,
            description,
            author: user.firstName,
            state,
            reading_time: readingTime(body),
            tags: tags,
            body,
        })

        // Save article to list of user articles
        user.articles.push(article.title)
        await user.save()
        await article.save()

        // Return article data
        return res.json({
            status: "success",
            message: `${user.firstName} ${user.lastName} created "${article.title}"`,
            data: article
        });
    }
    catch(err){
        res.status(500).send(err.message)
    }
}

To create a new article details like the title, description, body, and tags are obtained from the request body and used to create an article with the respective fields. Other fields like the reading_time, author, and id are auto-generated with the readingTime function, checkUser function, and the mongoose library respectively.

const deleteArticle = async (req, res) => {

    // Get article from database
    const article = await Article.findOne({ __id: req.params.id })
    const user = await checkUser(req, res)

    // Check if current user is the author of the article
    if(user.firstName !== article.author){
        return res.status(401).send({
            message: "You are not authorized to delete this article"
        })
    }

    // Remove article from list of user articles
    const userArticles = user.articles
    for(let i = 0; i < userArticles.length; i++){
        if(userArticles[i] == article.title){
            userArticles.splice(i, 1)
        }
    }

    await user.save()
    await Article.findByIdAndDelete(req.params.id)

    // Return success message
    res.json({
        status: "success",
        message: `${article.title} was deleted`,
    })
}

The deleteArticle function deletes a single article by searching through the database with the given id and removing it. It also removes the article from the array of the user's articles, it returns a success message when all this is completed.

const updateArticle = async (req, res) => {
    // Get details from request body
    const { title, description, state, tags, body } = req.body;

    const user = await checkUser(req, res)
    const article = await Article.findById(req.params.id)

    // check if current user is the author of the article
    if(user.firstName !== article.author){
        return res.send("You are not authorised to update this article")
    }

    // Update article
    const updatedArticle = await Article.findByIdAndUpdate({ _id: req.params.id }, {
        $set: {
            title,
            description,
            state,
            tags,
            body
        }
    }, { new: true })

    // Return updated article
    res.status(200).json({
        status: "success",
        message: `${updatedArticle.title} was updated`,
        data: {
            updatedArticle
        }
    })
}

The updateArticle updates a single article in the database with the information passed into the request body. This action is only allowed by the owner of the article, so before carrying out the procedure we first check if the name of the current user is the same as the author of the article.

// Export all the functions
module.exports = {
    getAllArticles,
    getMyArticles,
    getArticle,
    createArticle,
    deleteArticle,
    updateArticle,
}

After all the functions are completed, they are exported so they can be connected to their respective endpoints in the routes folder.

After checking that these functions are working properly by connecting them to a database and testing them with postman, we will proceed to implement JWT authentication.

JWT authentication

As the API is now, users will need to log in every time they want to use the application, but with JSON Web Tokens(JWT) we can save the user information and allow users to access the website by simply checking if they have the token.

For this API I used cookies to store the JWT in the browser, to do this we will use the jsonwebtoken package. A file named jwt will house the code for the jwt implementation:

// Required packages
const jwt = require('jsonwebtoken');
const User = require('../models/userModel')
require('dotenv').config();

We start by requiring the userModel object as this is what the JWT interacts with for signing in and logging in, lastly, we will require the dotenv package, which allows us to work with environmental variables

Notice how the dotenv package is not assigned to a variable, it is just declared with the .config attachment. This means the file is configured to accept environmental variables, which are called with the process.env method. E.g process.env.JWT_SECRET calls the JWT_SECRET variable from the .env file.

const maxAge = 60 * 60;

// Function for creating a token
const createToken = (id) => {
    return jwt.sign({ id }, process.env.JWT_SECRET, {
        expiresIn: maxAge
    });
}

The createToken function returns a jwt token which is set to expire in the time set by the maxage variable, the jwt.sign() function takes in two parameters: the information to be signed id, and a secret which we will need when decoding the token.

// Function for authenticating a route
const requireAuth = (req, res, next) => {
    const token = req.cookies.jwt;

    if(!token){
        console.log("No token")
        return res.json({
            status: "failed",
            message: "You need to be logged in to continue this action"
        })
    }

    jwt.verify(token, process.env.JWT_SECRET, (err, decodedToken) => {
        if(err){
            console.log(err.message)
            return res.redirect('/api/v1/login');
        } else {
            next();
        }
    })   
}

Some endpoints will require authentication, hence I created a requireAuth function as a middleware which makes an endpoint only accessible to logged-in users. The requireAuth function decodes the token which is stored in a cookie in the request object, and then verifies it, if successful the request proceeds to the endpoint, else the user is redirected to the login route.

// Function for checking the user
const checkUser = async (req, res) => {
    const token = req.cookies.jwt;

    if(!token){
        console.log('No token, please login');
        return null
    }

    const decodedToken = jwt.verify(token, process.env.JWT_SECRET)

    const user = await User.findById(decodedToken.id)
    return user
}

The checkUser function decodes the jwt token obtains the decoded id and uses it to check through the database and obtain the current user. This information can then be used to carry out many things, for example, it can be used to automatically get the name of the current user and assign it to the author property whenever the user creates a blog. It can also be used to display the current author.

module.exports = {
    createToken,
    requireAuth,
    checkUser
};

The jwt functions are then exported out of the file so they can be used wherever they are needed.

Implementing the jwt functions in the userControllers file:

const User = require('../models/userModel');
const { createToken } = require('../middleware/jwt');
const bcrypt = require('bcrypt')

const signup = async (req, res) => {

    /**
     * Function to create a new user
     * 
     * If the user already exists, log them in
     * 
     * Creates a token with a maxAge of 1 hour
     * 
     * Saves the token into a cookie called "jwt"
     * 
     * 
     * If any error occurs send the error message to the user
     */
    const user = await User.findOne({ email: req.body.email })
    if(user){
        console.log("This user already exists, you have been logged in!")
        return res.redirect('/api/v1/user/login')
    }

    try{
        const user = new User(req.body);
        const salt = await bcrypt.genSalt(10);
        user.password = await bcrypt.hash(user.password, salt);
        await user.save();

        const token = createToken(user._id);
        res.cookie('jwt', token, { maxAge: 60 * 60 * 1000 });

        return res.status(201).json({
            status: "success",
            message: "Sign up successful!, you have been automatically logged in",
            data: {
                firstName: user.firstName,
                lastName: user.lastName,
                email: user.email,
                articles: user.articles,
                id: user._id
            }
        })
    }
    catch(err){
        res.status(400).send(err.message)
    }
}

const login = async (req, res) => {

    /**
     * Function to log in a registered user
     * 
     * Creates a token with a maxAge of 1 hour
     * 
     * Saves the token into a cookie called "jwt"
     * 
     * 
     * If any error occurs send them to the user because they are the user's problems now 
     */
    const { email, password } = req.body;

    try{
        const user = await User.login(email, password);

        const token = createToken(user._id);

        res.cookie('jwt', token, { maxAge: 60 * 60 * 1000 });
        res.status(201).json({
            status: "success",
            message: "You logged in successfully",
            data: {
                firstName: user.firstName,
                lastName: user.lastName,
                email: user.email,
                articles: user.articles,
                id: user._id
            }
        });
    }
    catch(err){
        res.status(400).send(err.message)
    }
}
/**
 * Function to log out a user
 * 
 * Sets the jwt cookie to an empty string that expires in 1 millisecond
 */
const logout = (req, res) => {
    res.cookie('jwt', "", { maxAge: 1 });
    res.send('Logged out successfully')
}

module.exports = {
    signup,
    login,
    logout
}

The signup function now adds a user to the database and redirects them to the login route, which has a function that creates a jwt and also saves it to a cookie. For the logout function, the jwt is removed by setting the value of the cookie to an empty string that expires instantly, this is because cookies cannot be deleted directly.

Implementing the authentication in the userRoutes file:

// USER ROUTES

const express = require('express')
const userController = require('../controllers/userController');
const { requireAuth } = require('../middleware/jwt')

const userRouter = express.Router();

userRouter.post('/signup', userController.signup);

userRouter.post('/login', userController.login);

userRouter.post('/logout', requireAuth, userController.logout); // Secured

module.exports = userRouter;

Here only the logout function is authenticated, as a user will need to be logged in, to be logged out, ironic right?

In the article routes file the following routes will be authenticated:

const express = require('express')
const blogController = require('../controllers/blogController');
const { requireAuth } = require('../middleware/jwt');

const blogRouter = express.Router();

blogRouter.get('/', blogController.getAllArticles); // Not secured

blogRouter.get('/user', requireAuth, blogController.getMyArticles); // Secured

blogRouter.get('/:id', blogController.getArticle); // Not secured

blogRouter.post('/', requireAuth, blogController.createArticle); // Secured

blogRouter.delete('/:id', requireAuth, blogController.deleteArticle); // Secured

blogRouter.patch('/:id', requireAuth, blogController.updateArticle); // Secured

module.exports = blogRouter;

These routes need to be secured as requested by the project instructions, the only exception is the /user route which obtains all the articles by the logged-in user. I protected this route because it uses the jwt token to obtain the results, and it would throw an error if the user was not logged in.

Problems I faced

Jwt auth

I faced a lot of problems with the jwt authentication as this was my first time using it, I tried following a lot of articles but I had trouble because my project structure was different from the ones in the tutorial, and navigating through the documentation was a tough thing for a beginner.

Code architecture

I constantly had to change my codebase to match the new features I was trying to implement. For instance, for me to write tests I would need to export the entire server to be able to properly test all the endpoints. So I had to create a new file, server.js which would import the app connect to the database, and then listen on the port.

const app = require ("./src/app");

const mongoose = require('mongoose');
require("dotenv").config()

const PORT = process.env.PORT || 4000
const URL = process.env.DB_URL

//Connect to database
mongoose.connect(URL, { useNewUrlParser: true })
.then(() => console.log('Connected to database.....'))
.catch((err) => console.log('An error occured:', err.message))

app.listen(PORT, () => {
    console.log(`Server is running on http://localhost:${PORT}`)
})

Conclusion

The major takeaway I had is how there are multiple ways to do the same thing, from analyzing the projects of my peers to the codebase I saw in the various tutorials I watched, even though they were doing the same thing (building a simple blog API) they used varying methods to achieve different things.

The project was an experience to remember, and I am looking forward to building more projects in the future and sharing my journey.

Check out the GitHub repo to view the entire source code for the project

The code in the GitHub repo may differ from the ones in the article, as I am constantly improving the API and working toward applying views in the project.

Thank you for reading