This is part two of the Test-driven Development with Nodejs, Express, Mongoose & Jest, in part one, we set up our development environment and ran our first test. In this tutorial, we will focus on building the endpoints using the MVC Architecture.
Definition of terms
MVC - Model View Controler
Model View Controller is a software architectural pattern that involves the separation of the application logic into three interconnected elements the Model, View, and Controller.
Restful API
REST is an acronym for Representation State Transfer, API on the other hand is an acronym for Application Programme Interface. A RESTful API is an architectural style for an application program interface (API) that uses HTTP requests to access and use data.
I surmised you are familiar with the terms involved in this tutorial. Let's get started.
In the previous tutorial, we had a very basic folder structure, with few dependencies. Let's install the required dependencies for our API development.
$ npm i --save-dev body-parser dotenv nodemon
After installation, your package.json file should look like the one below.
{
"name": "tdd-with-nodejs",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest",
"start": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"body-parser": "^1.19.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"jest": "^26.6.3",
"mongoose": "^5.11.9",
"nodemon": "^2.0.6"
}
}
You notice we have configured our initialization script to run with nodemon, nodemon will keep track of every change made to our index.js file and refresh our application accordingly. Next, Let's set up a server. create a new file within the root directory of your application named** index.js** and paste the code below.
require('dotenv').config();
const mongoose = require("mongoose");
//const articles = require("./routes/article.routes");
const bodyParser = require("body-parser");
const app = exepress();
const port = 8000;
mongoose.connect(process.env.mongoURI, {useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true })
.then(res => console.log(`Connection Succesful ${res}`))
.catch(err => console.log(`Error in DB connection ${err}`));
//body-parser config;
app.use(exepress.json());
app.use(bodyParser.urlencoded({extended: true }));
app.use(bodyParser.json());
app.get("/", (req, res) => {
res.send(`<h1>Hello!</h1>`)
});
app.listen(port, () => {
console.log(`Application is listening at port ${port}`);
});
//register the enpoints
//app.use("/api/v1/articles", articles);
Don't forget to create a .env file and add your database URI like so: mongoURI=mongodb+srv://your-db-uri. Next, start the application by typing the command below on your terminal.
$ npm run start
You should get a response on your terminal that reads: Application is listening at port 8000 & Connection Succesful [object Object]. If you open http://localhost:8000 you should get "Hello!" logged to your screen as well.
This tutorial intends to teach you how to properly structure your Nodjs application to fit into the MVC pattern, so, we would be separating our business logic from our controller and routes files. We will learn more about this in the Layered Structure tutorial which is the final tutorial for this series.
Building the Model (Article.js)
Next, let's create our Model. Create a new folder within the root directory of the project, navigate into the folder, and create a new file named Article.js, and copy-paste the code below to create the model for our API.
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const articleSchema = Schema({
title:{
type: String,
required: true,
},
body:{
type: String,
required: true,
},
article_image: {
type: String,
required: false,
},
date:{
type: Date,
default: Date.now(),
}
});
module.exports = Article = mongoose.model("Article", articleSchema);
Our model is very basic, it has a title, body, and date object. You can learn more about building MongoDB Schemas using Mongoose by reading the official doc here.
Building the Article Service (ArticleService.js)
To build the ArticleService.js you need to create a folder named services to house our ArticleService.js file. Copy-paste the code below into your ArticleService.js.
const Article = require("../models/Article");
module.exports = class ArticleService{
static async getAllArticles(){
try {
const allArticles = await Article.find();
return allArticles;
} catch (error) {
console.log(`Could not fetch articles ${error}`)
}
}
static async createArticle(data){
try {
const newArticle = {
title: data.title,
body: data.body,
article_image: data.article_image
}
const response = await new Article(newArticle).save();
return response;
} catch (error) {
console.log(error);
}
}
static async getArticlebyId(articleId){
try {
const singleArticleResponse = await Article.findById({_id: articleId});
return singleArticleResponse;
} catch (error) {
console.log(`Article not found. ${error}`)
}
}
static async updateArticle(title, body, articleImage){
try {
const updateResponse = await Article.updateOne(
{title, body, articleImage},
{$set: {date: new Date.now()}});
return updateResponse;
} catch (error) {
console.log(`Could not update Article ${error}` );
}
}
static async deleteArticle(articleId){
try {
const deletedResponse = await Article.findOneAndDelete(articleId);
return deletedResponse;
} catch (error) {
console.log(`Could ot delete article ${error}`);
}
}
}
Building the controller (article.controller.js)
Next, lets' start writing our API endpoints, create a new folder within your root directory named controllers, navigate into the folder, and create a new file named article.controller.js. copy-paste the code below.
const ArticleService = require("../services/ArticleService");
module.exports = class Article{
static async apiGetAllArticles(req, res, next){
try {
const articles = await ArticleService.getAllArticles();
if(!articles){
res.status(404).json("There are no article published yet!")
}
res.json(articles);
} catch (error) {
res.status(500).json({error: error})
}
}
static async apiGetArticleById(req, res, next){
try {
let id = req.params.id || {};
const article = await ArticleService.getArticlebyId(id);
res.json(article);
} catch (error) {
res.status(500).json({error: error})
}
}
static async apiCreateArticle(req, res, next){
try {
const createdArticle = await ArticleService.createArticle(req.body);
res.json(createdArticle);
} catch (error) {
res.status(500).json({error: error});
}
}
static async apiUpdateArticle(req, res, next){
try {
const comment = {}
comment.title = req.body.title;
comment.body = req.body.body;
comment.articleImage = req.body.article_image
const updatedArticle = await ArticleService.updateArticle(comment);
if(updatedArticle.modifiedCount === 0){
throw new Error("Unable to update article, error occord");
}
res.json(updatedArticle);
} catch (error) {
res.status(500).json({error: error});
}
}
static async apiDeleteArticle(req, res, next){
try {
const articleId = req.params.id;
const deleteResponse = await ArticleService.deleteArticle(articleId)
res.json(deleteResponse);
} catch (error) {
res.status(500).json({error: error})
}
}
}
Building the routes (article.routes.js)
To communicate with our endpoints we need to set up our routes with corresponding requests. Create a new folder named routes, inside the folder, create a new file named article.routes.js. You can choose to name the folder whatever you like but it is always nice to maintain meaningful directory and file names.
const express = require("express");
const router = express.Router();
const ArticleCtrl = require("../controllers/article.controller");
router.get("/", ArticleCtrl.apiGetAllArticles);
router.post("/", ArticleCtrl.apiCreateArticle);
router.get("/article/:id", ArticleCtrl.apiGetArticleById);
router.put("/article/:id", ArticleCtrl.apiUpdateArticle);
router.delete("/article/:id", ArticleCtrl.apiDeleteArticle);
module.exports = router;
If you've followed the tutorial up to this point, your folder structure should actually look like the snippet below.
├── tdd-with-nodejs
├── controllers
├── article.controller.js
├── models
├── Article.js
├── routes
├── article.routes.js
├── services
├── articleService.js
├── test
Putting it all together
When working with the MVC pattern always ensure you maintain the Separation of Concern (SoC) technique. Separation of concerns (SoC) is a design principle for separating software applications into distinct sections such that each section addresses a separate concern. A concern is a set of information that affects the code of a Software Application. We will dive deeper into this topic in the next tutorial which is the last for this series.
Before we conclude let's test one of the endpoints. Send a POST request to the /api/v1/articles endpoint using an API testing tool of your choice. In my case, Postman You should get the response object as a response for your newly created article similar to the snippet below.
That's all for this tutorial. congratulations on making it thus far in this series. See you in the next tutorial.
Top comments (30)
Great article!
I will allow myself to make little one suggestion to your article for will make it a diamond.
To enable syntax highlighting in a code section, you needs to open your code block as
```javascript
code
```
Its will look likes
Very helpful. Will try it out
Thank you so much for this input, I will update the article shortly.
Great article and a good example with a modern approach. I learned a lot. So thanks for putting all this together.
Few questions though:
and what about the views-part?
Good news for tech devs!
I built and deployed a CLI to help you architect a Nodejs application using the MVC design pattern & the Layered Architecture. Contributions are welcomed #cli #npm #npmpackage
install using:
npm i -g @eetukudo/node-mvc
npmjs.com/package/@eetukudo/node-mvc
Emmanuel this is a great post!
I would like to get your honest feedback of my MVC framework. I have several years (6) applying the MVC concept on top of the express module. This is the latest version I made with ES6 best practices.
Dev.to Article
dev.to/digitalprimeint/mvc-framewo...
Documentation:
npmjs.com/package/plugdo-mvc
It could be 10 min of your time!
I will install the package and get back.
This is a great example of MVC with Node.
What’s the reasoning behind exporting a class with all static methods instead of just exporting the methods themselves?
It simply developer preference, The first thing that came to mind was the "Don't Repeat Yourself (DRY)" approach. Exporting each method can as well become verbose. Let me know if you can improve the code. Thanks.
I think exporting a class with all its methods is quite bulky if we only need one or two of them. Security concern might also exposed. Anyway, it's a good article 👍 Thank you
Very nice article. I will restructure my current project to follow this "layered" structure. I'm trying to become more aware of separating concerns. Hopefully, I'll reap the benefits early on. Thank for the article 🙂
Thank you so much @tonyscruze . The first benefit is that you can write testable code on the fly.
This is by far the best overview of MVC architecture in express that I have come across. Seriously, like a year of trying to get my head around it and this overview and actually really concise and clever syntax (like use of static class methods) has really helped me figure this out! thank you so much 🤟🤟💕
I've just started exploring the MVC architecture, it seems a little overwhelming linking the different folder/file structures to each other. This is a great article to adding a bit more clarity to my confusion but again I've come across a whole new "services" folder and I've not used it in my CRUD todo application.
What I'm uncertain of is, where to begin when creating an app using the MVC concept.
I tend to get confused about which file is to be linked/imported where. Perhaps if you can give me a few pointers that'd be great or maybe point me to another article or perhaps a resource. Thanks
Emmanuel this is a great post!
But I would like to ask a question.
If we already had try catch block at Controller, then do we need to implement them again in Service layer? There is a similar case study on a youtube channel name "Laravel Daily" (link https://www.youtube.com/watch?v=0hyRw7zeVcs&t=182s) and at 4:07 the video author (Povilas) said "A Service should just throw an exception". So is it not a good practice to catch exception anywhere else beside Controller?
What do you think about it?
Thank you,
Quy
A reference on how to configure Mongo locally or on cloud will be great, this made me stop reading it as tutorial and switch to a "he's just explaining a pattern" mindset. Good enough though, thanks!
Thank you @thxv3n0lvl for the feedback, I will write an article on how to create a MongoDB database on MongoDB-Atlas and managing MongoDB databases with MongoDB compass.