Create Node JS Project Mongodb Express API
Create Node JS Project Mongodb Express API

Not so long ago I create 2 projects with nested comments. With React and with Angular. A lot of people wrote me that they are interested in the backend part of this application. And they want to know what is the correct schema to implement such API.

Frontend

This is how our frontend looks like. We have a list of comments, we can create new comments, remove them and update if 5 minutes didn't pass.

Now in this post we will create a fully functional API by using Express and Mongodb for nested comments of any application.

Setting up project

Here I have an empty server.js file and package.json.

First of all we want to create a command to start our API.

"scripts": {
  "start": "nodemon src/index.js"
},

Here we used nodemon so our webserver will be automatically restarted after file change. Now we must install nodemon (for reload) and express as our framework for the backend.

npm i nodemon
npm i express

Now let's register and start our webserver.

const express = require("express");
const app = express();
app.listen(3012, () => {
  console.log("API is started");
});

Now with npm start we can start our API and it will be automatically reloaded.

Let's try to register new route just for testing.

app.get("/", (_, res) => {
  res.send("Hello API");
});

Now we can just to http://localhost:3012 and get there our API message. And actually I use Postman tool to test API requests.

Postman

If you don't know Postman is a super popular tool to test API requests.

Configuring database

Our next step here is to configure MongoDB database. And I don't want to spend here time on installing MongoDB on your machine. For the purpose of this post you just need to download and install it on your machine and run along with our API.

To work with MongoDB we must install a package.

npm i mongodb

Now we must bind MongoDB to our project. But I want to do it in the correct way. The main idea is that we want to use MongoDB instance from many places. This is why we want simply write it inside our server.js. I want to create a file which we can import everywhere.

// src/db.js
const { MongoClient } = require("mongodb");

const state = {
  db: null,
};

exports.connect = async (url, dbname) => {
  try {
    if (state.db) {
      return;
    }

    const client = new MongoClient(url);

    await client.connect();

    state.db = client.db(dbname);
  } catch (err) {
    console.error(err);
  }
};

exports.get = () => {
  return state.db;
};

Here we get MongoClient from mongodb package and created 2 functions get and connect. We will use get everywhere to get the instance of Mongodb and use it everywhere.

Inside connect function we are connecting to MongoDb and saving our instance of MongoDB to state.db so we can use it everywhere.

Now it is important to connect to MongoDB before we start our API because we want to be sure that we are connected and we can do requests to our database.

// src/index.js
const { connect } = require("./db");
...
const startServer = async () => {
  await connect("mongodb://localhost:27017/comments");

  app.listen(3012, () => {
    console.log("API is started");
  });
};

startServer();

We created additional startServer function where we wait for connection to database and only after we start our API.

Getting list of comments

Now we can start implementing our comments feature. And the first request that I want to do is to get all our comments. But for structuring our code I want to use MVC. We will create a controller and a model to split our code.

// src.index.js
const commentsController = require("./controllers/comments");
...
app.get("/comments", commentsController.all);

So commentsController is just a file with functions. Instead of writing callbacks directly in index.js we move split them between different controllers.

// src/controllers/comments.js
exports.all = async (req, res, next) => {
};

This is how our controller action looks like. But we won't write here directly our requests to database. We want to isolate them inside model.

// src/models/comments.js
const db = require("../db");

exports.all = () => {
  return db.get().collection("comments").find().toArray();
};

Here we created our model and defined all method to get a list of comments from the database. This is the only place where we define how we work with database.

Now we can use this method inside our controller.

// src/controllers/comments.js
const Comments = require("../models/comments");
exports.all = async (req, res, next) => {
  try {
    const docs = await Comments.all();
    res.send(docs);
  } catch (err) {
    next(err);
  }
};

Here we just used this method and returned an array to the client.

But it would be nice to normalize our data to get rid of underscores in _id.

// src/controllers/comments.js
const normalizeComment = (doc) => {
  const comment = { ...doc };
  comment.id = doc._id;
  delete comment._id;
  return comment;
};

exports.all = async (req, res, next) => {
  try {
    const docs = await Comments.all();
    const response = docs.map((doc) => normalizeComment(doc));
    res.send(response);
  } catch (err) {
    next(err);
  }
};

Here we just removed _id and added normal id. After this we called normalizeComment in our method.

As you can see in browser our data is empty but it will be normalized and returned to the client when we add some comments later.

Creating comments

We successfully implemented getting a list of our comments. Now we need to implement creating of our comments. But for this we must install additional package because we need to parse the body. And this package is called body-parser so we can parse our JSON correctly.

yarn add body-parser
// src/index.js
const bodyParser = require("body-parser");
...
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

It will now parse correctly body JSON and encoded urls.

Now we need to create a new method in our model.

// src/models/comments.js
...
exports.create = async (comment) => {
  const result = await db
    .get()
    .collection("comments")
    .insertOne({ ...comment, createdAt: new Date() });

  return db.get().collection("comments").findOne({ _id: result.insertedId });
};

Here we wrote a create method which get a comment that we want to create and writes it in database with adding a createdAt field. Most importantly we don't get a full created comment back but just an insertedId. This is why in order to return a comment back we must get it afterwards.

Now let's update our controller and define a new method there.

// src/controllers/comments.js
...
exports.create = async (req, res, next) => {
  try {
    const comment = {
      body: req.body.text,
      parentId: req.body.parentId || null,
      userId: "1",
    };
    const doc = await Comments.create(comment);
    const response = normalizeComment(doc);
    res.send(response);
  } catch (err) {
    next(err);
  }
};

Here we prepared a new comment by using body from the req. After this we created a comment and normalized it before building a response. But must importantly here is to set userId. In a real project with authentication your backend will know with what user you are authorized and will set correct ID accordingly. As we don't have any authentication in our project we simply wrote there 1 just for correct schema.

And last but not least we must register new route in our index.js.

app.post("/comments", commentsController.create);

Creating comment

Updating comments

Now let's implement updating of the comments. And it will be super similar to the create. First of all let's implement our model method.

// src/models/comments.js
...
exports.update = async (id, newData) => {
  await db
    .get()
    .collection("comments")
    .updateOne({ _id: ObjectId(id) }, { $set: newData });

  return db
    .get()
    .collection("comments")
    .findOne({ _id: ObjectId(id) });
};

We are doing here exactly the same. We get and ID of the comment and data that we want to update and update our comment. As we don't get comment back after the update we must get it again.

Now let's add our controller method.

// src/controllers/comments.js
...
exports.update = async (req, res, next) => {
  try {
    const doc = await Comments.update(req.params.id, { body: req.body.text });
    const response = normalizeComment(doc);
    res.send(response);
  } catch (err) {
    next(err);
  }
};

And we need to register this new route.

app.put("/comments/:id", commentsController.update);

Updating comment

Deleting a comment

The last thing that we need to implement is deleting of the comment. First of all let's start with a model.

// src/models/comments.js
...
exports.delete = (id) => {
  return db
    .get()
    .collection("comments")
    .deleteOne({ _id: ObjectId(id) });
};

Now let's update our controller.

// src/controllers/comments.js
...
exports.delete = async (req, res, next) => {
  try {
    await Comments.delete(req.params.id);
    res.sendStatus(200);
  } catch (err) {
    next(err);
  }
};

And don't forget to register a route.

app.delete("/comments/:id", commentsController.delete);

Deleting comment

And actually if you are interested to know how we implemented the frontend for this project make sure to check this post also.

📚 Source code of what we've done