Building A Todo API With NodeJs and Heroku-PostgreSQL

Building A Todo API With NodeJs and Heroku-PostgreSQL

In this article, we will build simple Todo APIs where users can perform the CRUD (CREATE, READ, UPDATE, and DELETE) operations using NodeJs, Express, and Heroku-PostgreSQL.

Prerequisites

  • Node: there is a must to have node installed on the computer. Click here, to download.
  • Virtual Studio Code: this is where we will also be writing out codes and testing. Click here, to download it on your computer.
  • Postman: we will be testing our API endpoints and documenting, it's very necessary to have it installed on our computer. Click here, to download.

Getting Started with the Project

For this project, we will be working on the ubuntu environment and using the Linux command to create a directory on the terminal for the project and also opening the created directory with VS Code using the following command:

$ mkdir todo-api ; cd todo-api;
$ code .

After launching the VS Code with the above command, fire up the integrated terminal with ctrl + ` and run the following command to initialize and install all the necessary dependencies needed for the project:

$ npm init -y
$ npm install express cors uuid bcrypt jsonwebtoken joi pg dotenv

you can also install nodemon which is going to be used as a dev-dependency to automatically restart our server every time we make changes to the project: $ npm install nodemon --save-dev

Your package.json file should look like this:

Screenshot from 2022-10-22 16-04-50.png

Structure the Project

Create the following files and directories as shown in the image below Screenshot from 2022-10-22 16-12-59.png

Server Setup

In the app.js file, setup the server using express and listen to a port in which you can save the value inside the .env file, here is now you can go about it:

import express from "express";
import dotenv from "dotenv";
import cors from "cors"
dotenv.config();

import userRouter from "./routes/userRoute.js";
import todoRouter from "./routes/todoRoute.js";
import errorHandler from "./utils/errorHandler.js";

const app = express();
const port = process.env.PORT || 6060;

app.use(cors())
app.use(express.json());

app.get("/", (_, res) => {
    res.json({ message: "TODO API is Running!!!" });
});

app.use("/api/v1/users", userRouter);
app.use("/api/v1/todos", todoRouter);

app.use(errorHandler);

app.listen(port, () => {
    console.log(`App listening on port: http://localhost:${port}`);
});

Connecting to Database

This project will be hosted on a backend service hosting called Heroku you can read the documentation on how to get started with NodeJs. And we will be making use of Heroku-PostgreSQL to store users' data. Add the following codes to the db.js file to connect to the database:

import pg from "pg";
import dotenv from "dotenv";
dotenv.config();

const { Pool } = pg;

const pool = new Pool({
    user: process.env.User,
    host: process.env.Host,
    database: process.env.Database,
    password: process.env.Password,
    port: process.env.Port,
    connectionString: process.env.DATABASE_URL,
    ssl: {
        rejectUnauthorized: false,
    },
});

pool.connect(function (err) {
  if (err) {
    return console.error("error: " + err.message);
  }
  console.log("Connected to POSTGRESQL server.");
});

export default pool;

Start the server with this command npm run dev and you should get this output:

Screenshot from 2022-10-22 17-09-07.png

Todo Routes, Controllers, and model Definition

The todo routes contain all the endpoints on the project, open the file todoRoutes.js and add the following codes:

import express from "express";
import authMiddleware from "../middleware/auth.middleware.js";
import controller from "../controllers/todoController.js"
const router = express.Router();

router.post("/", authMiddleware.authenticate, controller.createTodo);
router.get("/", authMiddleware.authenticate, controller.getTodos);
router.get("/:id", authMiddleware.authenticate, controller.getTodo);
router.patch("/:id", authMiddleware.authenticate, controller.updateTodo);
router.delete("/:id", authMiddleware.authenticate, controller.deleteTodo);

export default router;

The todo controller file is where you have the functions for each request to the endpoints... add the following codes to the file todoController.js

import { v4 } from "uuid";
import pool from "../database/db.js";
import AppException from "../utils/appException.js";
import {
    createTodoQuery,
    deleteTodoQuery,
    findTodoQuery,
    userTodosQuery,
} from "../utils/todoQueries.js";

/*---- Create A New Todo -----*/
const createTodo = async (req, res, next) => {
    try {
        const { title, description, due_date } = req.body;

        const newTodo = {
            id: v4(),
            user_id: req.userId,
            title: title,
            description: description,
            due_date: due_date,
            created_at: new Date(),
            updated_at: new Date(),
        };

        await pool.query(createTodoQuery, [
            newTodo.id,
            newTodo.user_id,
            newTodo.title,
            newTodo.description,
            newTodo.due_date,
            newTodo.created_at,
            newTodo.updated_at,
        ]);

        return res.status(201).json({
            message: "Todo Created Successfully",
        });
    } catch (err) {
        next(err);
    }
};

/*---- Fetch All Todos Created By The User -----*/
const getTodos = async (req, res, next) => {
    try {
        const findTodos = await pool.query(userTodosQuery, [req.userId]);

        return res.status(200).json(findTodos.rows);
    } catch (err) {
        next(err);
    }
};

/*---- Fetch User's Todo By Id -----*/
const getTodo = async (req, res, next) => {
    try {
        const { id } = req.params;

        const findTodo = await pool.query(findTodoQuery, [id]);
        if (!findTodo.rowCount) {
            throw new AppException(404, "Todo Not Found")
        }
        if (findTodo.rows[0].user_id !== req.userId) {
            throw new AppException(403, "Unauthorized")
        }
        return res.status(200).json(findTodo.rows[0]);
    } catch (err) {
        next(err);
    }
};

/*---- Update User's Todo -----*/
const updateTodo = async (req, res, next) => {
    try {
        const { id } = req.params;
        const updated_at = new Date();
        const findTodo = await pool.query(findTodoQuery, [id]);
        if (!findTodo.rows.length) {
            throw new AppException(404, "Todo Not Found")
        }
        if (findTodo.rows[0].user_id !== req.userId) {
            throw new AppException(403, "Unauthorized")
        }

        let updateTodos = "UPDATE todos SET updated_at = $1, ";
        const updateKeys = Object.keys(req.body);
        let values = [updated_at];
        let param = 2;

        updateKeys.forEach((key, index) => {
            const value = req.body[key];
            updateTodos += `${key} = $` + param;
            param++;
            if (index < updateKeys.length - 1) {
                updateTodos += ", ";
            }
            values.push(value);
        });
        values.push(id);
        let lastParam = updateKeys.length + 2;
        updateTodos += " WHERE id = $" + lastParam;
        console.log(updateTodos, values);
        pool.query(updateTodos, values);

        return res.status(200).json({ message: "Todo Updated Successfully" });
    } catch (err) {
        next(err);
    }
};

/*---- Delete User's Todo -----*/
const deleteTodo = async (req, res, next) => {
    try {
        const { id } = req.params;
        const findTodo = await pool.query(findTodoQuery, [id]);
        if (!findTodo.rows.length) {
            throw new AppException(404, "Todo Not Found")
        }
        if (findTodo.rows[0].user_id !== req.userId) {
            throw new AppException(403, "Unauthorized")
        }
        pool.query(deleteTodoQuery, [id]);

        return res.status(410).json({
            message: "Todo Successfully Deleted",
        });
    } catch (err) {
        next(err);
    }
};

export default { createTodo, getTodos, getTodo, updateTodo, deleteTodo };

The Todo model is where the schema of the todo table is and the codes should be as follows:

-- USER TABLE --
CREATE TYPE stats AS ENUM ('Pending', 'Completed');
CREATE TABLE todos
(
    id VARCHAR(36) UNIQUE PRIMARY KEY,
    user_id VARCHAR(36),
    title VARCHAR(50) NOT NULL,
    description VARCHAR(100) NOT NULL,
    status stats DEFAULT 'Pending',
    due_date TIMESTAMP NOT NULL,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

Authentication and Validation

Authentication was done using JSON web token, the following codes should be in the file auth.middleware.js:

import Jwt from "jsonwebtoken";
import dotenv from "dotenv";
dotenv.config();

const authenticate = async (req, res, next) => {
    try {
        const authorization = req.headers.authorization;
        if (!authorization) {
            return res.status(401).json({ message: "Access Denied" });
        }
        const authenticationArr = authorization.split(" ");
        if (authenticationArr[0] !== "Bearer") {
            return res.status(401).json({ message: "Access Denied" });
        }
        const token = authenticationArr[1];
        if (!token) {
            return res.status(401).json({ message: "Access Denied" });
        }
        Jwt.verify(token, process.env.SECRET, (err, payload) => {
            if (err) {
                return res.status(400).json({message: "Invalid Token"});
            } else {
                req.userId = payload.id;
            }
        });

        next();
    } catch (err) {
        return res.status(500).json({ message: err.message });
    }
};

export default { authenticate };

Validation with joi is as follows in the file validate.middleware.js:

 import Joi from "joi";

const validateSignUP = Joi.object({
  name: Joi.string().required(),
  email: Joi.string().email({
    minDomainSegments: 2,
    tlds: { allow: ["com", "net"] },
  }).required(),
  password: Joi.string().min(8).required(),
});

const validateSignIn = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required(),
});

export { validateSignUP, validateSignIn };
  • Authenticaticating the user is very important because we need to identify the user that is accessing the app and to make sure that the user is authorized to use the app
  • Validation is also an important feature for this project to make sure that the user is inputting the correct details that would be saved in the database the way we want it saved.

Testing Of Endpoints

Testing was done using Postman, to make sure we are sending the right request to the correct endpoint and also getting back the correct response. You can find the testing of the APIs in this Documentation

Screenshot from 2022-10-22 18-40-29.png

Hosting on a Remote Server

The project was deployed on Heroku and the server URL was passed to the Frontend Developer for implementation

Screenshot from 2022-10-22 18-41-53.png

Conclusion

In this article, we built a Todo API using NodeJs and Heroku-PostgreSQL. You can find the complete source code in the GitHub link.

Please feel free to leave a comment if you have any questions regarding this project.

Thank you for reading

Contributors

Abiodun Shittu

Ugochukwu Chioma