Building a Blog API with Node.js: Implementing the API Routes (Part 4)
Tags: node, api, mongodb, tutorial
Published: 2023-01-09 17:55:27

Now that we have our API design complete, the models set up, the database connection established and the authentication strategies in place, it's time to start building the actual API. In this article, we'll cover the following topics:

* Implementing the routes for our API
    
* Writing the controllers to handle requests and send responses
    
* Testing the API with Postman
    

Let's get started!

## Implementing the Routes

1. Create a file called `blog.routes.js` in the `/routes` directory and write this code:
    
    ```javascript
    const express = require("express");
    const { blogController } = require("../controllers");
    
    const blogRouter = express.Router();
    
    blogRouter.get("/", blogController.getPublishedArticles);
    
    blogRouter.get("/:articleId", blogController.getArticle);
    
    module.exports = blogRouter;
    ```
    
    The `express.Router()` function creates a new router object that can be used to define routes that will be used to handle HTTP requests to the server.
    
    The first route defined is a GET route at the root path of the router. This route will handle HTTP GET requests to the server and will use the `getPublishedArticles` function from the `blogController` to return a list of all published articles.
    
    The second route is a GET route that includes a parameter `:articleId` in the path. This route will handle HTTP GET requests to the server with a specific article ID in the path, and will use the `getArticle` function from the `blogController` to return the article with the specified ID.
    
2. Create a file called `author.routes.js` in the `/routes` directory and write this code:
    
    ```javascript
    const express = require("express"); 
    const { authorController } = require("../controllers");
    const {
    	newArticleValidationMW,
    	updateArticleValidationMW,
    } = require("../validators");
    
    const authorRouter = express.Router();
    
    // create a new article 
    authorRouter.post("/", newArticleValidationMW, authorController.createArticle);
    
    // change state 
    authorRouter.patch(
    	"/edit/state/:articleId",
    	updateArticleValidationMW,
    	authorController.editState
    );
    
    // edit article
    authorRouter.patch(
    	"/edit/:articleId",
    	updateArticleValidationMW,
    	authorController.editArticle
    );
    
    // delete article 
    authorRouter.delete("/delete/:articleId", authorController.deleteArticle);
    
    // get all articles created by the author 
    authorRouter.get("/", authorController.getArticlesByAuthor);
    
    module.exports = authorRouter;
    ```
    
    Here, we've imported the validation middleware we created in the [previous article](https://dev.to/pda/building-a-blog-api-with-nodejs-authentication-and-validation-part-3-p4n):
    
    ```javascript
    const {
    	newArticleValidationMW,
    	updateArticleValidationMW,
    } = require("../validators");
    ```
    
    When a POST or PATCH request is made, the validation middleware checks if the request body is in the correct format:
    
    ```javascript
    authorRouter.post("/", newArticleValidationMW, authorController.createArticle);
    
    authorRouter.patch(
    	"/edit/state/:articleId",
    	updateArticleValidationMW,
    	authorController.editState
    );
    authorRouter.patch(
    	"/edit/:articleId",
    	updateArticleValidationMW,
    	authorController.editArticle
    );
    ```
    
3. Create a file called `auth.routes.js` in the `/routes` directory for the authentication routes:
    
    ```javascript
    const express = require("express");
    const passport = require("passport");
    const { userValidator } = require("../validators");
    
    const { authController } = require("../controllers");
    
    const authRouter = express.Router();
    
    authRouter.post(
    	"/signup",
    	passport.authenticate("signup", { session: false }), userValidator,
    	authController.signup
    );
    
    authRouter.post("/login", async (req, res, next) =>
    	passport.authenticate("login", (err, user, info) => {
    		authController.login(req, res, { err, user, info });
    	})(req, res, next)
    );
    
    module.exports = authRouter;
    ```
    
    The `signup` route uses the `passport.authenticate()` function to authenticate the request using the `signup` strategy. It also specifies that session management should be disabled using the `{ session: false }` option. If the request is successfully authenticated, it is passed to the `authController.signup` function for further processing.
    
    The `login` route is similar, but it uses the `login` strategy and passes the request to the `authController.login` function if it is successfully authenticated.
    
    Both routes also use the `userValidator` middleware to validate the request body before it is passed to the controller functions.
    
4. Create an `index.js` file in `/routes` and export the routers:
    
    ```javascript
    const blogRouter = require("./blog.routes"); 
    const authorRouter = require("./author.routes");
    const authRouter = require("./auth.routes");
    
    module.exports = { blogRouter, authorRouter, authRouter };
    ```
    
5. Import the routers and use them in the `app.js` file.
    
    ```javascript
    // {... some code ....}
    const { blogRouter, authRouter, authorRouter } = require("./src/routes");
    
    // Middleware
    // {... some code ....}
    
    // Routes
    app.use("/blog", blogRouter);
    app.use("/auth", authRouter);
    app.use(
    	"/author/blog",
    	passport.authenticate("jwt", { session: false }),
    	authorRouter
    );
    
    // {... some code ....}
    ```
    
    Remember we separated the routes into blog routes and author routes because we wanted to protect the author routes. We are using passport to protect the `/author/blog` routes, so that only authenticated users can have access to the resources. Unauthenticated users can only have access to the `/blog` routes.
    

## Writing the Controllers

Now that we have the routes defined, we need to write the code to handle the requests and send the appropriate responses. A controller is a function that is called when a route is matched, and it is responsible for handling the request and sending the response.

1. Create a file called `blog.controllers.js` in the `/controllers` directory. In this file, define the functions that will be called by the blog routes:
    
    ```javascript
    const { BlogModel, UserModel } = require("../models");
    
    // Get all published articles
    exports.getPublishedArticles = async (req, res, next) => {
    	try {
    		const {
    			author,
    			title,
    			tags,
    			order = "asc",
    			order_by = "timestamp,reading_time,read_count",
    			page = 1,
    			per_page = 20,
    		} = req.query;
    
    		// filter
    		const findQuery = { state: "published" };
        
            if(author) {
              findQuery.author = author;
            }
    		if (title) {
    			findQuery.title = title;
    		}
            if (tags) {
              const searchTags = tags.split(",");
              findQuery.tags = { $in: searchTags };
            }
    
    		// sort
    		const sortQuery = {};
    		const sortAttributes = order_by.split(",");
    
    		for (const attribute of sortAttributes) {
    			if (order === "asc") {
    				sortQuery[attribute] = 1;
    			}
    			if (order === "desc" && order_by) {
    				sortQuery[attribute] = -1;
    			}
    		}
    
    		// get all published articles from the database
    		const articles = await BlogModel.find(findQuery)
    			.populate("author", "firstname lastname email")
    			.sort(sortQuery)
    			.skip(page)
    			.limit(per_page);
    
    		return res.status(200).json({
    			message: "Request successful",
    			articles: articles,
    		});
    	} catch (error) {
    		return next(error);
    	}
    };
    
    // Get article by ID
    exports.getArticle = async (req, res, next) => {
    	const { articleId } = req.params;
    	try {
    		const article = await BlogModel.findById(articleId).populate(
    			"author",
    			"firstname lastname email"
    		);
    		
    		article.readCount += 1; // increment read count
    		await article.save();
    
    		return res.status(200).json({
    			message: "Request successful",
    			article: article,
    		});
    	} catch (error) {
    		return next(error);
    	}
    };
    ```
    
    The `getPublishedArticles` function will be called when the `GET /blog` route is matched, and it will retrieve a list of all published articles from the database and send the response to the client. It begins by destructuring some query parameters from the `req.query` object. These query parameters are used to filter and sort the articles that are returned.
    
    The sorting in this function is done using the Mongoose `.sort()` method, which takes an object with the field names as keys and the sort order as values. The field names and sort orders are specified in the `sortQuery` object, which is constructed based on the `order` and `order_by` query parameters.
    
    The `order_by` query parameter is a string that can contain multiple field names, separated by commas. The `sortAttributes` variable is an array of these field names. For each field name in the array, the `sortQuery` object is updated with the field name as the key and the sort order as the value. If the `order` query parameter is `"asc"`, the sort order is set to 1, and if the order query parameter is `"desc"`, the sort order is set to -1.
    
    The find query is constructed using the `findQuery` object, which is initialized with a filter to only return published articles. The `findQuery` object is then updated with additional filters based on the `author`, `title`, and `tags` query parameters, if they are present.
    
    For example, if the `author` query parameter is present, the `findQuery.author` field is set to the value of the `author` query parameter. This will filter the results to only include articles whose `author` field is equal to the value of the `author` query parameter. Similarly, if the `tags` query parameter is present, the `findQuery.tags` field is set to a MongoDB operator that searches for articles with tags that match the search tags. The search tags are specified as a comma-separated string and are split into an array using the split method. The `$in` operator is used to search for articles with tags that are included in the array of search tags.
    
    The `findQuery` object is passed as an argument to the `.find()` method of the `BlogModel` object. This method returns a Mongoose query object that can be used to find documents in the blog collection that match the specified filter. The `.populate()` method is then called on the query object to populate the author field with the corresponding user document, and `.sort()` sorts the results using the `sortQuery` object. `.skip()` and `.limit()` are called to paginate the results. `skip()` specifies the number of documents to skip in the query, and `limit()` specifies the number of documents to return. The `page` and `per_page` query parameters are used to determine the values passed to `skip()` and `limit()`. By default, `page` is set to 1 and `per_page` is set to 20. This means that if these query parameters are not provided, the default behavior is to retrieve the first page of results with 20 documents per page.
    
    The `getArticle` function retrieves the article with the specified ID from the database and increments the article's read count before returning it to the client.
    
    Both functions use the `async` keyword and include a `try/catch` block to handle any errors that may occur during the execution of the function. If an error occurs, the `next` function is called with the error as an argument to pass the error to the error handling middleware which is set up in our `app.js`.
    
2. Create a file called `author.controllers.js` in the `/controllers` directory and define the functions that will be called by the author routes as follows:
    
    ```javascript
    const { BlogModel, UserModel } = require("../models");
    const { blogService } = require("../services");
    const moment = require("moment");
    
    // create a new article
    exports.createArticle = async (req, res, next) => {
    	try {
    		const newArticle = req.body;
    
    		// calculate reading time
    		const readingTime =
                blogService.calculateReadingTime(newArticle.body);
    
            if (newArticle.tags) {
              newArticle.tags = newArticle.tags.split(",");
            }
            if (req.files) {
              const imageUrl = await blogService.uploadImage(req, res, next);
              newArticle.imageUrl = imageUrl;
            }
    
    		const article = new BlogModel({
    			author: req.user._id,
    			timestamp: moment().toDate(),
    			readingTime: readingTime,
    			...newArticle,
    		});
    
    		article.save((err, user) => {
    			if (err) {
    				console.log(`err: ${err}`);
    				return next(err);
    			}
    		});
    
    		// add article to user's articles array in the database
    		const user = await UserModel.findById(req.user._id);
    		user.articles.push(article._id);
    		await user.save();
    
    		return res.status(201).json({
    			message: "Article created successfully",
    			article: article,
    		});
    	} catch (error) {
    		return next(error);
    	}
    };
    
    // change state
    exports.editState = async (req, res, next) => {
    	try {
    		const { articleId } = req.params;
    		const { state } = req.body;
    
    		const article = await BlogModel.findById(articleId);
    
    		// check if user is authorised to change state
    		blogService.userAuth(req, res, next, article.author._id);
    
    		// validate request
    		if (state !== "published" && state !== "draft") {
    			return next({ status: 400, message: "Invalid state" });
    		}
    
    		if (article.state === state) {
    			return next({ 
                    status: 400, 
                    message: `Article is already in ${state} state` 
                });
    		}
    
    		article.state = state;
    		article.timestamp = moment().toDate();
    
    		article.save();
    
    		return res.status(200).json({
    			message: "State successfully updated",
    			article: article,
    		});
    	} catch (error) {
    		return next(error);
    	}
    };
    
    // edit article
    exports.editArticle = async (req, res, next) => {
    	try {
    		const { articleId } = req.params;
    		const { title, body, tags, description } = req.body;
    
    		// check if user is authorised to edit article
    		const article = await BlogModel.findById(articleId);
    		blogService.userAuth(req, res, next, article.author._id);
    
    	    // if params are provided, update them
            if (req.files) {
              const imageUrl = await blogService.uploadImage(req, res, next);
              article.imageUrl = imageUrl;
            }
    		if (title) {
    			article.title = title;
    		}
    		if (body) {
    			article.body = body;
    			article.readingTime = blogService.calculateReadingTime(body);
    		}
    		if (tags) {
    			article.tags = tags.split(",");
    		}
    		if (description) {
    			article.description = description;
    		}
    		if (title || body || tags || description ) {
    			article.timestamp = moment().toDate();
    		}
    
    		article.save();
    
    		return res.status(200).json({
    			message: "Article successfully edited and saved",
    			article: article,
    		});
    
    	} catch (error) {
    		return next(error);
    	}
    };
    
    // delete article
    exports.deleteArticle = async (req, res, next) => {
    	try {
    		const { articleId } = req.params;
    
    		const article = await BlogModel.findById(articleId);
    
    		// check if user is authorised to delete article
    		blogService.userAuth(req, res, next, article.author._id);
    
    		article.remove();
    
    		return res.status(200).json({
    			message: "Article successfully deleted",
    		});
    	} catch (error) {
    		return next(error);
    	}
    };
    
    // get all articles created by the user
    exports.getArticlesByAuthor = async (req, res, next) => {
    	try {
    		const {
    			state,
    			order = "asc",
    			order_by = "timestamp",
    			page = 1,
    			per_page = 20,
    		} = req.query;
    
    		const findQuery = {};
    		// check if state is valid and if it is, add it to the query
    		if (state) {
    			if (state !== "published" && state !== "draft") {
    				return next({ status: 400, message: "Invalid state" });
    			} else {
    				findQuery.state = state;
    			}
    		}
    
    		// sort
            const sortQuery = {};
    
    		if (order !== "asc" && order !== "desc") {
    			return next({ 
                    status: 400, 
                    message: "Invalid parameter order" 
                });
    		} else {
              sortQuery[order_by] = order;
            }
    
    		// get user's articles
    		const user = await UserModel.findById(req.user._id).populate({
    			path: "articles",
    			match: findQuery,
    			options: {
    				sort: sortQuery,
    				limit: parseInt(per_page),
    				skip: (page - 1) * per_page,
    			},
    		});
    
    		return res.status(200).json({
    			message: "Request successful",
    			articles: user.articles,
    		});
    	} catch (error) {
    		return next(error);
    	}
    };
    ```
    
    The `createArticle` function begins by destructuring the new article data from the `req.body` object and calculating the reading time for the article using the `calculateReadingTime` function from the `blogService` module which we'll create next to abstract some functionalities.
    
      
    Next, the function checks if the `tags` field is present in the `newArticle` data, and splits the tags string into an array using the `split` method. Recall that `tags` is defined as an array in the blog model.
    
      
    Next, If the user uploads a cover image, we want to access the file, upload it to a cloud storage service, get the URL for the uploaded image and save it to our database. The `express-fileupload` we installed will allow us to access the `req.files` object.
    
      
    Finally, the function creates a new `BlogModel` instance with the `newArticle` data and the current timestamp and user ID, and saves it to the database using the `save` method. We're using an npm module `moment` to handle the `timestamp`. You can install moment by running `npm install moment`.  
    
    The `author` field is an `ObjectId` referencing the `User` model. Here, we assign the user's ID so we can use Mongoose `.populate` method to retrieve the author's details when we need to.  
    
    Similarly, we want to be able to track all the articles created by the user, so next, we update the user's `articles` array in the database to include the ID of the new article before we return a response to the client.  
    
    In the `editState` function, we retrieve the article from the database and check if the user is authorized to change the state of the article. If the user is authorized, we update the article's state and timestamp in the database.  
    
    In the `editArticle` function, we retrieve the article from the database and check if the user is authorized to edit the article. If the user is authorized, we update the article's data in the database.  
    
3. Create a file called `auth.controllers.js` in the `/controllers` directory for the authentication routes:
    
    ```javascript
    const jwt = require("jsonwebtoken");
    const { UserModel } = require("../models");
    
    exports.signup = async (req, res, next) => {
    	const { firstname, lastname, email, password } = req.body;
    
    	try {
    		const user = new UserModel({
    			firstname: firstname,
    			lastname: lastname,
    			email: email,
    			password: password,
    		});
    
    		user.save((err, user) => {
    			if (err) {
    				return next(err);
    			}
    		});
    
    		delete user.password;
    
    		return res.status(201).json({
    			message: "Signup successful",
    			user: user,
    		});
    	} catch (error) {
    		return next(error);
    	}
    };
    
    exports.login = (req, res, { err, user, info }) => {
    	if (!user) {
    		return res.status(401).json({ message: "email or password is incorrect" });
    	}
    
    	req.login(user, { session: false }, async (error) => {
    		if (error) return res.status(401).json({ message: error });
    
    		const body = { _id: user._id, email: user.email };
    
    		const token = jwt.sign({ user: body }, process.env.JWT_SECRET, {
    			expiresIn: "1h",
    		});
    
    		return res.status(200).json({ 
                message: "Login successful",
                token: token 
            });
    	});
    };
    ```
    
    The `signup` function creates a new `user` object with the provided first name, last name, email, and password. It then saves this `user` object to the database and returns a success message and the `user` object, minus the `password` field. You should never expose passwords.  
    
    The `login` function is called when a user attempts to log in. If the provided email and password do not match a user in the database, it returns a 401 status code and an error message. Otherwise, it creates a `JWT` with the user's ID and email as the payload, and signs it with a secret key (which we stored in our `.env` file. Since we're not using session, logging out is tricky, so we've set an expiration time of one hour for the token. The function returns a success message and the `JWT`.
    
4. Create a file called `/index.js` in the `/controllers` directory:
    
    ```javascript
    const authController = require("./auth.controller"); 
    const blogController = require("./blog.controller"); 
    const authorController = require("./author.controller");
    
    module.exports = { 
        authController, 
        blogController, 
        authorController 
    };
    ```
    

### Blog Service

We abstracted some of the logic for our `author.controller.js` to a `blogService` module. Let's look into that.

Before we begin, we're using Cloudinary, a media management platform that allows you to store, optimize, and deliver images and videos. Follow the instructions on the [Cloudinary website](https://cloudinary.com/documentation/how_to_integrate_cloudinary) to set up your account, then install the SDK by running this command:

```javascript
npm install cloudinary
```

Now, in `/services`, create a file called `blog.service.js` with the following functions:

```javascript
const cloudinary = require('cloudinary').v2;

cloudinary.config({
  secure: true
});

exports.userAuth = (req, res, next, authorId) => {
	// if author is not the same as the logged in user, throw error
	if (req.user._id !== authorId.toString()) {
		return next({
			status: 401,
			message: "You are not authorized to access this resource",
		});
	}
};

exports.calculateReadingTime = (text) => {
	const wordsPerMin = 200;
	const wordCount = text.trim().split(/\s+/).length;
	const readingTime = Math.ceil(wordCount / wordsPerMin);

	return readingTime > 1 ? `${readingTime} mins` : `${readingTime} min`;
};

exports.uploadImage = async (req, res, next) => {
  const filePath = req.files.file.tempFilePath;

  const options = {
    use_filename: true,
    unique_filename: false,
    overwrite: true,
  };

  try {
    const uploadedImage = await cloudinary.uploader.upload(filePath, options);

    return uploadedImage.secure_url;
  } catch (error) {
    return next(error);
  }

}
```

The `userAuth` function checks if the user making the request is the same as the author of the post being accessed. If the user is not the author, it throws an error with a status code of 401 (Unauthorized).

The `calculateReadingTime` function takes a piece of text as an argument and calculates the estimated reading time in minutes by dividing the number of words in the text by the average number of words per minute (*the average reading speed for an adult is about 200wpm according to Marc Brysbaert from Ghent University in Belgium*). It then rounds up to the nearest whole number and returns a string with the reading time.

The `uploadImage` function uploads an image to Cloudinary and returns a secure URL. It takes `req`, `res` and `next` as arguments, retrieves the file path of the uploaded image from the `req` object and stores it in a `filePath` variable (this is made possible by the `express-fileupload` npm package we installed). It then sets up options for the upload, including the `use_filename` and `overwrite` options, which specify that the uploaded file should use its original filename and overwrite any existing file with the same name. The function then uses the `cloudinary.uploader.upload` method to upload the file to Cloudinary. If the upload is successful, it returns the secure URL of the uploaded file.

In `/services/index.js`:

```javascript
const blogService = require('./blog.service.js');

module.exports = {
  blogService
}
```

## Testing the API with Postman

With the routes and controllers in place, we can now test our API to make sure it's working as expected. You can use Postman to test all of the routes in your API to make sure they are working as expected.

To test routes that require a request body, such as POST and PATCH routes, you'll need to specify the request body in the appropriate format in the Postman request form. You'll also need to provide a bearer token in the `Authorisation` header when testing protected routes.

For example, if you're sending a POST request to the `/author/blog` route, include the article data in the request body.

* First, you'll need to start the server by running the following command in the terminal:
    

```plaintext
npm start
```

* Next, create a new user. Send a POST request to the `/auth/signup` route and include the user data in the request body like this:
    

```json
{
  "email": "doe@example.com",
  "password": "Password1",
  "firstname": "jon",
  "lastname": "doe",
}
```

If successful, you'll get a success response:

```json
{
    "message": "Signup successful",
    "user": {
        "firstname": "jon",
        "lastname": "doe",
        "email": "doe@example.com",
    }
}
```

* Next, login. Send a POST request to the `/auth/login` route and include the user data in the request body like this:
    

```json
{
  "email": "doe@example.com",
  "password": "Password1",
}
```

If successful, you'll get a success response with a token:

```json
{
    "message": "Login successful",
    "token": "sjlkafjkldsf.jsdntehwkljyroyjoh.tenmntiw"
}
```

* Finally, send POST request to the `/author/blog` route and include the article data in the request body like this:
    

```json
{
    "title": "testing the routes",
    "body": "This is the body of the article",
    "description": "An article",
    "tags": "blog,test",
}
```

You should get a 401(Unauthorised) status code. Try again, but add an `Authorization` header with a bearer token `Bearer {token}`. Replace `{token}` with the token you got after logging in. You should get a success response:

```json
{
    "message": "Article created successfully",
    "article": {
        "title": "testing the routes",
        "description": "An article",
        "tags": [
            "blog",
            "test"
        ],
        "author": "6366b10174282b915e1be028",
        "timestamp": "2022-11-05T20:52:40.573Z",
        "state": "draft",
        "readCount": 0,
        "readingTime": "1 min",
        "body": "This is the body of the article",
        "_id": "6366cd18b34b65410bc391db"
    }
}
```

Test the other routes.

That's it! With the API routes and controllers implemented and tested, we now have a fully-functional blog API. In the next article, we'll cover additional topics such as error logging and deploying our API. Stay tuned!