How I built a blog API and the technology stack

ยท

11 min read

Ever wondered how a blogging API works under the hood? Well, in this article I'll be showing you how it works, a typical setup up and the technology stack. Feel free to share your ideas in the comments on things that can be improved.

I built this API as part of my backend software engineering diploma coursework. I created a consumption-ready endpoint using a JavaScript cross-platform runtime environment - Node js.

Technology stack

The following is a blend of libraries, frameworks and packages I used to build the blog API:

  • Nodejs - a programming language for backend dev.

  • Expressjs - a framework for backend dev. ontop Nodejs

  • NPM - Node package manager

  • JWT - JSON Web Token

  • MongoDB (NoSQL DB) and Mongoose (ORM)

  • JOI - user input validation

  • Nodemon - Nodejs application runtime tooling

  • Bcrypt - password hashing and encrypting

Follow my step-by-step process

As with any Node backend application, I started by initializing the project using npm.

npm init -y

The "-y" flag was so I could skip the QnA interactive interface. Next, I installed the node packages and libraries I used in the project.

npm i express jsonwebtoken mongooose bycrypt joi cookie-parser dotenv winston

At the end of the process above, I have this in my package.json file :

On line 8, I created a script to help run my API server, listen for changes in the code and restart the server accordingly using the nodemon package as a dev dependency. You can read about nodemon here. The entry/index file into the project is set to server.js. Hence the start script will run "nodemon server.js".

Now, let's create a simple express server.

To do so, we need to require the express module. Thereafter, I'll set the port on my machine/PC on which to listen for requests. What are modules and ports in the context of backend software engineering?

A module is a simple or complex functionality organized into a single or multiple JavaScript files which can be reused throughout a JavaScript application.

A port in this case is an entry point into computer hardware for software programs to run and since Node is a cross-platform run time, it can run on most operating systems and servers.

Take a look at the server initialisation code below:

In line 1 above, using Common JS syntax, I required the express external module and also saved it in a variable named express.

In lines 3 and 4, I initialized the express framework in a variable named app and also declared a port for the server to run on my machine. Using the express initialized variable - app - I created an HTTP GET method in line 7 and on line 14. You can read more about HTTP methods here

The method starting in line 7 above created the index route. When a user hits or makes a request to the root route denoted by '/', it returns a response in JSON format. I've set it to return a simple message, "Welcome to the blogging app". The HTTP status code is 200 indicating a successfully processed request.

In line 14, I'm handling requests to routes that are not provided for in the API. By using the wild card character - "*" - any GET request to an unprovided route will return an HTTP 404 status code indicating "resource not found" or error route.

To provide other routes for specific resources in the blog API, I created new folders. Also, I adopted the model, views and controllers MVC design pattern for my folder structure. So, my folder structure looks like this:

You'll notice that I now have:

  • controllers: this is where I will be handling the backend logic.

  • middlewares: this is where I will be handling any process between a request and a response such as authentication, user input validation, JSON web token etc.

  • routes: this is where I will be creating other routes for specific responses within the API.

  • dbConfig: we'll get to this later.

Let's start with the two routes in this blogging API - the user routes and the blog routes. But first, let me explain what I'm trying to achieve. The blog routes will serve up blog content or posts created by a particular user as well as by other users. Of course, a user cannot edit blog content by others. The user routes will handle the creation of a user account on the database.

Let's get hacking...

In lines 7 and 8, I've required the two new routes I've created inside the routes folder. You'll notice this in the folder address. In lines 11 and 12 respectively, I've mounted two middlewares on the express application to allow for (1) JSON data transport over HTTP request and response headers and (2) to transport HTML form data over HTTP request and response headers.

In addition, on lines 13 and 14, I've created additional routes and set my express application to use it. So now, when users request /users or /blogs, they'll be served corresponding API resources.

Next, I'll be creating a route for the user signup process. The user route expects users to provide a first name, last name, username and password to sign up. I'll be using an npm package called Joi to validate user input. I already installed this package. Learn more about Joi here

Let's get hacking...

I've created a middleware folder and within it is a userInputValidation.js file. Here, I've required Joi, created a middleware function that will intercept the request before going to the controller function and I've exported it as a module. I've made the function asynchronous to make the code execution wait for a promise before exiting the middleware.

Inside the Joi.object() method on line 5, I've created an object containing the keys corresponding to the expected user input during account creation. I've set the values to Joi value type which in this case is a string, is required and is an email. I applied Javascript method-chaining for each key and thereafter saved everything inside the variable named schema. Thereafter, I validated the request body by calling the validateAsync on the schema variable and passing req.body inside it in line 12 along with a couple of options.

If this is successful, the code execution flows to the next function using the next() method on line 17. This function is the controller function. Now, I'll use this validUserCreation function inside the userRoute file as shown below:

Notice in line 3 above that I've imported the middleware function and I've called/used it on line 6 inside a post request as a second parameter. By doing this, I mounted the middleware. Hence, the code execution will listen for inputs from the user on the /signup route.

Database integration

I used MongoDB for this project along with Mongoose Object Document Mapping (ODM). MongoDB is a schema-less NoSQL document database. Mongoose helps to map data input into the database in an object format. The backend logic will be ineffective without a data storage service or component.

In this project, I used MongoDB Atlas - a cloud NoSQL db solution. I created an account, a project and a cluster. You can read more on getting started with MongoDB Atlas HERE.

Let's get hacking...

To connect the Atlas, I took the URI string and database name I'd already created in the account and saved all these variables inside the .env file in my code. The reason for doing this is that these credentials are secret and I don't want unauthorized access to it when I'm pushing my code to a remote repository like GitHub. Next, I imported these variables into a dbConfig.js file and called the variables using Node environment module process.env.

Inside the asynchronous function, a try and catch code block connects to the database and logs a successful connection message or an unsuccessful connection message, and the error - whichever the case - to the console. I then exported the function as a module to be called inside the index file which is named server.js. This is so that the backend connects to the database as soon as the server starts to run.

Next, I created the data model which will be used to map data into the DB. See the screenshot below for the code implementing this.

Don't fret! I'll explain what these 46 lines of code do. I've imported Mongoose and Bcrypt in lines 1 and 2. Both are npm external modules. I'll be using Bcrypt to encrypt user password before saving it into the database. This ensures that user accounts are secure even if the database is breached.

Using the Mongoose ODM, I created a schema instance and I've used this instance to create the data object mapping, data type and options (i.e. required or not). On line 26, you'll notice that I used a Mongoose-provided method or hook .pre that allows action before saving into the database. So, within this hook, I used Brcypt inside a try and catch code block to salt and hash the password.

I also added another method to the UserSchema called isValidPassword. The method will be used during user login process to compare the user-provided password with one encrypted in the database by first decrypting and then comparing. On line 43, I called Mongoose model function, passed in the NoSQL database collection name which I've called "users" and the schema which I created earlier on line 5 named UserSchema.

Now, let's go back into our controller function where I'll show you how I'll create the logic that will handle the user creation process.

Up till now, I have created user input validation, a database model and a database connection. Following the MVC architecture, I'll create the controller function in a file in a separate folder and export the module which I'll use inside the router file. This maintains a clear design pattern.

From lines 1 to 3, I've imported npm external module JsonWebToken, the user model created earlier and the environment variables. What's happening in this code?

I've created an asynchronous function that takes req & res parameters. This means that when a post request hits the /signup route, the user input will be validated by Joi npm module, if successful, the next() function will pass request to the controller function above. This is the final bus stop in the code execution.

Inside the controller function I named createUser above, I've created another variable named newUserInput to house the user input which is inside the req.body object. Next, I ran a check in the database to determine if the email of the user already exists on line 15 to 19. If so, I returned a 409 error code and a message: "User already exists". If not, the code execution continues and on line 22, I call the .create() on the UserModel asynchronously and passed the user-provided input/details. This will store the data in the database.

But, what is happening between lines 29 to 36? Once a user is successfully entered into the database, I signed a JWT token using the user password, email and a JWT secret string which I've securely kept as an environment variable. All of this is saved in the token variable which is returned to the client using res.cookie() with the cookie name and token passed as parameters. Next, we return a final response to the client comprising a 201 resource-created status code, a success message, the user object and the token. All these inside a json object. Where there is an error, I handled this error inside the catch code block and responded accordingly.

Following this is the module.exports statement on lines 51 to 53. Thereafter, I'll import the module inside the route file and use it like this โฌ‡๏ธ

You'll notice I imported the controller function on line 4 and used it on line 10 after the Joi input validation middleware.

Now, let's see some code in action. First, I'll start the server by running the code below in the VSCode terminal:

npm start

This is the result:

You'll notice inside the orange box that the server started successfully on port 3000 and the connection to MongoDB was also successful.

Next, let's test the user sign-up route on Postman.

In section 1 of the screenshot, I made a POST request to the users/signup route passing into the post request body details of the user accounts to be created in section 2. It was successful as you see in section 3 where the server and database operation returned 201 status code. Within this section, I logged the message, userObject created and the token. You'll also notice that the password is longer than what it was in section 2. This is as a result of the hashing.

Next is the login route.

I imported the necessary modules namely JWT, userModel and environment variable. Within the parameterized login function, I listen for a request and stored such in a variable name userLoginDetail. Next, I ran a check in the database to see if user-provided email exists, if not, I returned a 404 not found error message. If found, I started the login process by validating the password. Where the credentials are accurate, I assigned a JWT with a 1-hour expiration. Next, I returned a success message, user object and the token. Finally, I catch any exceptions or errors in the catch code block.

After that, I followed the same process to create a blog data Model, route and validation. However, for the security of the API, I protected the create, update and delete blog routes using a bearer token middleware function. See the screengrab below:

Notice that on lines 13, 16, 19 and 22, I have a validation.bearerTokenAuth middleware. What is happening within the validation.bearerTokenAuth module? Let's have a look together:

Within this function, I listened for the JWT assigned to each user during login which I expect to receive in the req.headers. If this is not present, access is revoked and a 401 status code is returned with a message: "You are not authorized!".

Where provided, I unwrapped or decoded the bearer token and also verified it using jwt.verify() method. Finally, I returned the user object inside the request body and called the next() function to move the controller function.

If you have followed thus far, I appreciate you reading to the end of the walkthrough. If you have any questions, do hit me up. See the link to the code below:

Source code: https://github.com/Olanrewaju-dev/blogging_api_altsch

Cheers ๐Ÿ‘‹๐Ÿฝ๐Ÿ‘‹๐Ÿฝ

ย