Lucky victory
Lucky Victory's Blog

Follow

Lucky Victory's Blog

Follow
Build a Note Keeping API with NodeJS, Express, and HarperDB.

Build a Note Keeping API with NodeJS, Express, and HarperDB.

In the previous article, we built a quote generator API with HarperDB, but this time we are going to do something different, and much more interesting

Lucky victory's photo
Lucky victory
·May 7, 2022·

18 min read

Play this article

Table of contents

  • Introduction
  • What you will learn
  • Tools for this project.
  • Routes for the API
  • Conclusion

This article is part of a series titled APIs with NodeJS and HarperDB , which is focused on creating APIs with HarperDB, don't know about HarperDB? Then you can learn about it from this article.

Introduction

In the previous article, we built a quote generator api, but this time we are going to do something different, substantial, and much more interesting, we are going to build a Note Keeping API.

What you will learn

In this article, you will learn the following,

  • how to integrate API keys into an API.
  • how to validate the user-supplied API key.
  • how to generate a new API key for the user.
  • how to revoke and expire an API key.
  • Authentication and Authorization, how to authorize a user to access only their content.
  • how to use middleware in ExpressJS.

Without much ado let's dive into it.

if you prefer the finished project, you can find it on this Github Repo

Tools for this project.

  • VS Code as the Code Editor.
  • REST Client (a VS Code extension) for testing the API endpoints.

To start the project, open up the terminal and create a directory named note-api by running the following command,

mkdir note-api

and move into that directory

cd note-api

initialize a package.json for the project.

npm init -y

We will be needing a couple of dependencies and devDependencies for this project.

Let's install the necessary dependency modules we need for this project.

npm install express cors harpee dotenv morgan http-errors bcrypt nanoid uuid

those are the dependency modules, Let me explain what each module will do.

  • express: this module will be used to set up a server and routes that we need.
  • cors: this module will be used to make certain routes available so others on a different domain can use our API.
  • harpee: a modeling tool for harperDB (created by me) that will help simplify some procedures for the usage of HarperDB.
  • dotenv: will help us set up environment variables to use locally in our app.
  • morgan: to log information about each request to the console.
  • bcrypt: this module would be used to hash the user's password.
  • http-errors: to create and throw an error for unavailable routes.
  • nanoid: this module would be used to generate IDs for the notes.
  • uuid: we would use this module to generate unique api keys.

Let's also install a devDependency module.

npm install nodemon --save-dev

What this module does is, it will restart our server whenever we make changes while working locally.

Now let's create an entry index.js file, with the following boilerplate code

const express = require("express");
const port = process.env.PORT || 4420;
const app = express();


app.get("/", (req, res) => {
  res.send("hello notes API ");
});

app.listen(port, () => {
  console.log(`server running on port ${port}`);
});

Let's update our package.json file.

"scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js
    }

Now in the terminal, run the following command to start the server.

npm run dev

The application will be running at localhost:4420, and you should see this message.

Screenshot 2022-05-07 101537.png

So as usual, let's create some folders to stay organized,

  • config folder: this folder would include files for setting up our database.

  • models folder: this folder would include our schema and table setup.

  • middlewares folder: this folder would include middlewares for our API, hold up! don't know what middlewares are? Then you should read this article, middlewares in ExpressJS.

  • helpers folder: this is where we are going to write some helper functions to use across our API.
  • controllers folder: this folder would include functions for each route.
  • routes folder: this folder includes our routes setup.
  • rest folder: this folder would contain files for testing our API endpoints. if you're using VS Code, you will need to install the REST Client extension, otherwise, feel free to use your preferred API testing application, e.g Postman.

Let's also create a .env file, with our HarperDB instance information.

DB_HOST= your harperDB instance e.g https://xxxxxxxx.harperdbcloud.com
DB_USER= your harperDB instance username
DB_PASS= your harperDB instance password

Routes for the API

This API would include the following routes,

except for the account route, every other route requires an apikey to be passed as a query parameter, you will see that when we start testing the API.

  • /account
    • /sign-up [method=post]: to register a new user.
    • /sign-in [method=post] : to log in a user.
  • /notes[method=get]: fetch a user's notes.
    • /new [method=post]: add a new note.
    • /edit/:id [method=put]: edit a note by its ID.
    • /to-trash:id [method=put]: move a note to trash.
    • /out-of-trash:id [method=put]: remove a note from the trash.
  • /tokens
    • /new [method=post]: generate a new apikey.
    • /revoke/:apikeyToRevoke [method=put]: revoke an apikey.
    • /drop/:apikeyToDrop [method=delete]: discard an apikey.
  • /trash [method=get]: fetch notes in trash.
    • /trash/:noteID [method=delete]: permanently discard a note.

And those are the routes for this API.

Now in the models folder, Let's create 4 files because we will be working with 4 tables.

So first let's create a users.model.js file to set up the model for the users table,

const { Schema, Model } = require("harpee");

const NotesSchema = new Schema({
  name: "NotesSchema",
  fields: {
    firstName: String,
    lastName: String,
    joinedAt: Date,
    email: String,
    password: String,
  },
  primaryKey: "id",
});

const Users = new Model("Users", NotesSchema);

module.exports = Users;

apikeys.model.js file to set up the model for apikeys table,

const { Schema, Model } = require("harpee");

const NotesSchema = new Schema({
  name: "NotesSchema",
  fields: {
    userId: String,
    key: String,
    createdAt: Date,
    expiresIn: Date,
    status: String,
  },
  primaryKey: "id",
});

const ApiKeys = new Model("ApiKeys", NotesSchema);

module.exports = ApiKeys;

notes.model.js file to set up the model for the notes table,

const { Schema, Model } = require("harpee");

const NotesSchema = new Schema({
  name: "NotesSchema",
  fields: {
    userId: String,
    title: String,
    body: String,
    createdAt: Date,
    modifiedAt: Date,
    status: String,
  },
  primaryKey: "id",
});

const Notes = new Model("Notes", NotesSchema);

module.exports = Notes;

And lastly, the trash.model.js file to set up the model for the trash table,

const { Schema, Model } = require("harpee");

const NotesSchema = new Schema({
  name: "NotesSchema",
  fields: {
    userId: String,
    noteId: String,
    deletedAt: Date,
  },
  primaryKey: "id",
});

const Trash = new Model("Trash", NotesSchema);

module.exports = Trash;

That's it for the models setup.

Now, in the config folder, let's create some files to configure our database. db.config.js: where we can configure the database connection.

// so we can use environment variables 
require('dotenv').config();
const {createConnection}= require('harpee');

const dbConfig={
    host:process.env.DB_HOST,
    username:process.env.DB_USER,
    password:process.env.DB_PASS
}
const connectDB=()=>{
    createConnection(dbConfig);
}

module.exports={
    connectDB
}

db.init.js: to initialize our models so we can work with them.

const { connectDB } = require("./db.config");
// connect to the database
connectDB();
const ApiKeys = require("../models/apikeys.model");
const Notes = require("../models/notes.model");
const Trash = require("../models/trash.model");
const Users = require("../models/users.model");

const dbInit = async () => {
  await Users.init();
  await Notes.init();
  await Trash.init();
  await ApiKeys.init();
};
dbInit();
module.exports = dbInit;

In the helpers folder, let's create an index.js file with some helper functions

const uuid = require("uuid");
const ApiKeys = require("../models/apikeys.model");
const Notes = require("../models/notes.model");

/**
 * @param {string} email
 *
 */
const emailValidator = (email) => {
  const emailRegex =
    /^[-!#$%&'*+\/0-9=?A-Z^_a-z{|}~](\.?[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/;

  return emailRegex.test(email);
};
// get date after/ before current date, default is 30days after today
const getDateDiff = (diff = 30, after = true) => {
  const now = new Date();

  const dateDiff = after
    ? new Date(now.setDate(now.getDate() + diff))
    : new Date(now.setDate(now.getDate() - diff));
  return dateDiff.getTime();
};

const getDateInMilliseconds = (date = new Date().getTime()) =>
  new Date(date).getTime();

/**
 check if a value is null or undefined
 *
 */
const NullOrUndefined = (value) => {
  return (
    Object.prototype.toString.call(value) == "[object Null]" ||
    Object.prototype.toString.call(value) == "[object Undefined]"
  );
};
/**
 * check if a value is empty
 */
const isEmpty = (value) => {
  return NullOrUndefined(value) || !Object.keys(value).length || value === "";
};

/**
 *
 * generate api key
 */
const generateToken = () => {
  const key = uuid.v4();

  return key;
};
/**
 create new api key and store it in the database
 *
 */
const createNewApiKey = async (userId, expireIn = 30) => {
  if (isEmpty(userId)) throw new Error("'userId' is required");
  const key = generateToken();
  // expiry date in milliseconds
  const expiresIn = getDateDiff(expireIn);
  const createdAt = getDateInMilliseconds();
  await ApiKeys.create({
    userId,
    key,
    status: "active",
    expiresIn,
    createdAt,
  });
  return { apikey: key };
};

/**
 *
 * @param {string} id
 * @returns
 */
const getNoteById = async (id) => {
  try {
    const note = await Notes.findOne({ id }, [
      "id",
      "title",
      "body",
      "userId",
      "createdAt",
      "modifiedAt",
      "status",
    ]);

    if (!note) {
      return [null, `note with id '${id}' was not found`];
    }

    return [note, null];
  } catch (error) {
    return [null, error];
  }
};
/**
 * check if the user is authorized to access a resource
 */
const authorizeUser = (user, resource) => {
  if (resource.userId !== user.id) {
    return "Unauthorized, not allowed to access this resource";
  }
};

const getApiKeyFromDB = async (key) => {
  try {
    const userKey = await ApiKeys.findOne({ key }, [
      "id",
      "userId",
      "status",
      "expiresIn",
    ]);
    if (!userKey) {
      return [null, "invalid token"];
    }
    return [userKey, null];
  } catch (error) {
    return [null, error];
  }
};
module.exports = {
  getDateInMilliseconds,
  getDateDiff,
  getNoteById,
  NullOrUndefined,
  isEmpty,
  generateToken,
  createNewApiKey,
  authorizeUser,
  getApiKeyFromDB,
  emailValidator,
};

In the middlewares folder, let’s create an index.js file with the following code

const {
  getDateInMilliseconds,
  getApiKeyFromDB
} = require("../helpers");

const ApiKeys = require("../models/apikeys.model");
const Users = require("../models/users.model");

/**
*
 get token from request 'api_key' query params

*/
const getTokenFromQuery = (req, res, next) => {
  const authInQuery = req.query.api_key;
  if (authInQuery) {
    // append the token to request object, so it can be access by other middlewares
    req.token = authInQuery;
    next();
    return;
  }
  res.status(403).json({
    message: "No token provided "
  });
};
/**
get apikey from the database and validate it;

* */
const validateToken = async (req, res, next) => {
  try {
    const key = req.token;
    const [userKey, userKeyError] = await getApiKeyFromDB(key);
    if (userKeyError) {
      res.status(400).json({
        message: userKeyError
      });
      return;
    }
    // check if the key has expired
    const currentDate = getDateInMilliseconds();
    const hasExpired = currentDate >= getDateInMilliseconds(userKey.expiresIn);
    if (userKey && userKey.status == "revoked") {
      res.status(400).json({
        message: "this key has been revoked, please generate a new one",
      });
      return;
    } else if (userKey && hasExpired) {
      // updated Apikey table and set the token to 'expired'
      await ApiKeys.update([{
        id: userKey.id,
        status: "expired",
      }, ]);
      res.status(400).json({
        message: "your apikey has expired, you should generate a new one",
      });
      return;
    }
    req.userKey = userKey;
    next();
  } catch (error) {
    res.status(500).json({
      message: "an error occured",
      error
    });
  }
};

/**
*
* get user by 'id' and set it as a property to 'req' object
*
*/
const getUserById = async (req, res, next) => {
  try {
    const {
      userKey
    } = req;
    const user = await Users.findOne({
      id: userKey.userId
    }, ["id"]);
    if (!user) {
      res.status(404).json({
        message: "user not found"
      });
      return;
    }
    req.user = user;
    next();
  } catch (error) {
    res.status(500).json({
      message: "an error coccurred",
      error,
    });
  }
};

module.exports = {
  getTokenFromQuery,
  validateToken,
  getUserById,
};

In the controllers folder, We are going to create 4 files. users.controller.js

const bcrypt = require("bcrypt");

const {
  isEmpty,
  getDateInMilliseconds,
  createNewApiKey,
  emailValidator,
} = require("../helpers");

const Users = require("../models/users.model");

const getUserByEmail = async (email) => {
  try {
    const user = await Users.findOne({ email }, [
      "email",
      "password",
      "id",
      "firstName",
    ]);
    if (!user) {
      return [null, "User not found"];
    }
    return [user, null];
  } catch (error) {
    return [null, error];
  }
};

const createNewUser = async (req, res) => {
  try {
    let { email, password, firstName, lastName } = req.body;

    if (
      isEmpty(email) ||
      isEmpty(password) ||
      isEmpty(firstName) ||
      isEmpty(lastName)
    ) {
      return res.status(400).json({
        message: "please provide 'email','password', 'firstName','lastName'",
      });
    }
    // check if the email is valid
    const isValidEmail = emailValidator(email);
    if (!isValidEmail) {
      return res.status(400).json({
        message: "invalid email address",
      });
    }

    // check if user with email already exist
    const [user] = await getUserByEmail(email);
    if (user) {
      return res.status(400).json({
        message: "user already exist",
      });
    }
    // hash the password before storing in the database
    const hashedPassword = await bcrypt.hash(String(password), 10);
    password = hashedPassword;
    const joinedAt = getDateInMilliseconds();
    const newUser = {
      email,
      password,
      firstName,
      lastName,
      joinedAt,
    };
    // get the id of the new user
    const { inserted_hashes } = await Users.create(newUser);
    const userId = inserted_hashes[0];
    const { apikey } = await createNewApiKey(userId);

    res.status(200).json({
      message: "your registration was successful",
      apikey,
    });
  } catch (error) {
    res.status(500).json({
      error,
      message: "an error occurred",
    });
  }
};

const loginUser = async (req, res) => {
  try {
    const { email, password } = req.body;
    if (isEmpty(email) || isEmpty(password)) {
      return res.status(400).json({
        message: " 'email' and 'password' are required",
      });
    }

    const [user, userEmailErrorMessage] = await getUserByEmail(email);
    if (userEmailErrorMessage) {
      return res.status(404).json({
        message: userEmailErrorMessage,
      });
    }
    // compare password to see if it matches the one in the database
    const previousPassword = user.password;
    const passwordMatch = await bcrypt.compare(
      String(password),
      previousPassword
    );
    if (!passwordMatch) {
      return res.status(400).json({
        message: "invalid credentials",
      });
    }

    const { apikey } = await createNewApiKey(user.id);
    res.status(200).json({
      message: "login successful",
      apikey,
    });
  } catch (error) {
    res.status(500).json({
      error,
      message: "an error occurred",
    });
  }
};
module.exports = {
  createNewUser,
  getUserByEmail,
  loginUser,
};

notes.controller.js

const {
  isEmpty,
  getDateInMilliseconds,
  authorizeUser,
  getNoteById,
} = require("../helpers");
const Notes = require("../models/notes.model");
const Trash = require("../models/trash.model");
const {
  nanoid
} = require("nanoid");
const createNewNote = async (req, res) => {
  try {
    const {
      user
    } = req;
    const userId = user.id;
    if (isEmpty(req.body)) {
      return res.status(400).json({
        message: "please provide a note to add",
      });
    }
    const {
      title,
      body
    } = req.body;
    if (isEmpty(title) && isEmpty(body)) {
      return res.status(400).json({
        message: "can't save an empty note, include atleast `title` or `body` ",
      });
    }
    const createdAt = getDateInMilliseconds();
    const modifiedAt = createdAt;
    const newNote = {
      id: nanoid(25),
      userId,
      title,
      body,
      createdAt,
      modifiedAt,
      status: 'active'
    };
    const {
      inserted_hashes
    } = await Notes.create(newNote);
    res.status(201).json({
      message: `successfully added new note with id '${inserted_hashes[0]}'`,
    });
  } catch (error) {
    res.status(500).json({
      error,
      message: "an error occurred,couldn't add new note",
    });
  }
};
const getNotesByUser = async (req, res) => {
  try {
    const {
      user
    } = req;
    const notes = await Notes.find({
      getAttributes: ["id", "title", "body", "createdAt", "modifiedAt", "status"],
      where: `userId='${user.id}' `,
      and: " status='active'",
    });
    res.status(200).json({
      message: `successfully retrieved notes`,
      notes,
    });
  } catch (error) {
    res.status(500).json({
      message: "an error occurred,couldn't retrieve otes",
      error,
    });
  }
};
const editNote = async (req, res) => {
  try {
    const {
      user
    } = req;
    const {
      note_id
    } = req.params;
    const [note, noteErrorMessage] = await getNoteById(note_id);
    if (noteErrorMessage) {
      return res.status(404).json({
        message: noteErrorMessage,
      });
    }
    const authError = authorizeUser(user, note);
    if (authError) {
      return res.status(401).json({
        message: authError,
      });
    }
    // check if the body object is empty
    if (isEmpty(req.body)) {
      return res.status(400).json({
        message: "nothing to update",
      });
    }
    const noteToUpdate = req.body || {};
    noteToUpdate["id"] = note_id;
    noteToUpdate["modifiedAt"] = getDateInMilliseconds();
    await Notes.update([noteToUpdate]);
    res.status(200).json({
      message: `successfully updated note with id '${note_id}'`,
    });
  } catch (error) {
    res.status(500).json({
      message: "an error occurred,couldn't update note",
      error,
    });
  }
};
const moveNoteToTrash = async (req, res) => {
  try {
    const {
      user
    } = req;
    const {
      note_id
    } = req.params;
    const [note, noteErrorMessage] = await getNoteById(note_id);
    if (noteErrorMessage) {
      return res.status(404).json({
        message: noteErrorMessage,
      });
    }
    const authError = authorizeUser(user, note);
    if (authError) {
      return res.status(401).json({
        message: authError,
      });
    }
    await Notes.update([{
      id: note_id,
      status: 'deleted'
    }, ]);
    const deletedAt = getDateInMilliseconds();
    const noteToTrash = {
      deletedAt,
      userId: user.id,
      noteId: note.id,
    };
    await Trash.create(noteToTrash);
    res.status(200).json({
      message: `successfully moved note with id '${note_id}' to trash`,
    });
  } catch (error) {
    res.status(500).json({
      message: "an error occurred,couldn't move note to trash",
      error,
    });
  }
};
const moveNoteFromTrash = async (req, res) => {
  try {
    const {
      user
    } = req;
    const noteId = req.params.note_id;
    const [note, noteErrorMessage] = await getNoteById(noteId);
    if (noteErrorMessage) {
      return res.status(404).json({
        message: noteErrorMessage,
      });
    }
    const authError = authorizeUser(user, note);
    if (authError) {
      return res.status(401).json({
        message: authError,
      });
    }
    await Notes.update([{
      id: noteId,
      status: 'active'
    }, ]);
    // remove note from Trash table
    await Trash.findAndRemove({
      noteId
    });
    res.status(200).json({
      message: `successfully removed note with id ${noteId} from trash`,
    });
  } catch (error) {
    res.status(500).json({
      message: "an error occurred,couldn't move note to trash",
      error,
    });
  }
};
module.exports = {
  moveNoteFromTrash,
  createNewNote,
  editNote,
  getNotesByUser,
  moveNoteToTrash,
};

trash.controller.js

const {
  getNoteById,
  authorizeUser
} = require("../helpers");
const Notes = require("../models/notes.model");
const Trash = require("../models/trash.model");
const getNotesInTrash = async (req, res) => {
  try {
    const {
      user
    } = req;
    const notes = await Notes.find({
      getAttributes: ["id", "title", "body", "createdAt", "modifiedAt", "status", ],
      where: `userId='${user.id}' `,
      and: "status='deleted'",
    });
    res.status(200).json({
      message: `successfully retrieved notes`,
      notes,
    });
  } catch (error) {
    res.status(500).json({
      message: "an error occurred,couldn't retrieve notes",
      error,
    });
  }
};
const deleteNotesInTrash = async (req, res) => {
  try {
    const {
      user
    } = req;
    const noteId = req.params.note_id;
    const note = await Trash.findOne({
      noteId
    });
    if (!note) {
      return res.status(404).json({
        message: `note with id '${noteId}' was not found`,
      });
    }
    const authError = authorizeUser(user, note);
    if (authError) {
      return res.status(401).json({
        message: authError,
      });
    }
    await Trash.findAndRemove({
      noteId
    });
    await Notes.findByIdAndRemove([noteId]);
    res.status(200).json({
      message: "permanently deleted note from trash",
    });
  } catch (error) {
    res.status(500).json({
      message: "An error occurred",
      error,
    });
  }
};
module.exports = {
  deleteNotesInTrash,
  getNotesInTrash,
};

And lastly, apikeys.controller.js

const {
  createNewApiKey,
  authorizeUser,
  getApiKeyFromDB,
} = require("../helpers");
const ApiKeys = require("../models/apikeys.model");
const generateNewApiKey = async (req, res) => {
  try {
    const {
      user
    } = req;
    const {
      apikey
    } = await createNewApiKey(user.id);
    res.status(200).json({
      message: "successfully generated new apikey",
      apikey,
    });
  } catch (error) {
    res.status(500).json({
      error,
      message: "an error occurred",
    });
  }
};
const revokeApiKey = async (req, res) => {
  try {
    const key = req.params.token;
    const {
      user
    } = req;
    const [userKey, userKeyError] = await getApiKeyFromDB(key);
    if (userKeyError) {
      res.status(400).json({
        message: userKeyError,
      });
      return;
    }
    const authError = authorizeUser(user, userKey);
    if (authError) {
      return res.status(401).json({
        message: authError,
      });
    }
    await ApiKeys.update([{
      id: userKey.id,
      status: "revoked",
    }, ]);
    res.status(200).json({
      message: "key successfully revoked ",
    });
  } catch (error) {
    res.status(500).json({
      message: "an error occurred",
      error,
    });
  }
};
const dropApiKey = async (req, res) => {
  try {
    const key = req.params.token;
    const {
      user
    } = req;
    const [userKey, userKeyError] = await getApiKeyFromDB(key);
    if (userKeyError) {
      res.status(400).json({
        message: userKeyError,
      });
      return;
    }
    const authError = authorizeUser(user, userKey);
    if (authError) {
      return res.status(401).json({
        message: authError,
      });
    }
    await ApiKeys.findAndRemove({
      key
    });
    res.sendStatus(204);
  } catch (error) {
    res.status(500).json({
      message: "an error occurred",
      error,
    });
  }
};
module.exports = {
  generateNewApiKey,
  revokeApiKey,
  dropApiKey,
};

Now in the routes folder, we are going to create 4 files, one file for each route set up.

users.route.js

const express = require("express");
const { createNewUser, loginUser } = require("../controllers/users.controller");
const router = express.Router();
const cors = require("cors");

router.post("/sign-up", cors(), createNewUser);
router.post("/sign-in", cors(), loginUser);

module.exports = router;

notes.route.js

const express = require("express");
const router = express.Router();
const {
  createNewNote,
  editNote,
  getNotesByUser,
  moveNoteToTrash,
  moveNoteFromTrash,
} = require("../controllers/notes.controllers");
const {
  getTokenFromQuery,
  validateToken,
  getUserById,
} = require("../middlewares");
const cors = require("cors");

router.get(
  "/",
  cors(),
  getTokenFromQuery,
  validateToken,
  getUserById,
  getNotesByUser
);
router.post(
  "/new",
  cors(),
  getTokenFromQuery,
  validateToken,
  getUserById,
  createNewNote
);
router.put(
  "/edit/:note_id",
  cors(),
  getTokenFromQuery,
  validateToken,
  getUserById,
  editNote
);
router.put(
  "/to-trash/:note_id",
  cors(),
  getTokenFromQuery,
  validateToken,
  getUserById,
  moveNoteToTrash
);
router.put(
  "/out-of-trash/:note_id",
  cors(),
  getTokenFromQuery,
  validateToken,
  getUserById,
  moveNoteFromTrash
);

module.exports = router;

trash.route.js

const express = require("express");
const {
  getNotesInTrash,
  deleteNotesInTrash,
} = require("../controllers/trash.controller");
const {
  getTokenFromQuery,
  validateToken,
  getUserById,
} = require("../middlewares");

const router = express.Router();
const cors = require("cors");
router.get(
  "/",
  cors(),
  getTokenFromQuery,
  validateToken,
  getUserById,
  getNotesInTrash
);
router.delete(
  "/:note_id",
  cors(),
  getTokenFromQuery,
  validateToken,
  getUserById,
  deleteNotesInTrash
);

module.exports = router;

And lastly, apikeys.route.js

const express = require("express");
const {
  generateNewApiKey,
  revokeApiKey,
  dropApiKey,
} = require("../controllers/apikeys.controller");
const {
  getTokenFromQuery,
  validateToken,
  getUserById,
} = require("../middlewares");

const router = express.Router();

router.post(
  "/new",
  getTokenFromQuery,
  validateToken,
  getUserById,
  generateNewApiKey
);
router.put(
  "/revoke/:token",
  getTokenFromQuery,
  validateToken,
  getUserById,
  revokeApiKey
);
router.delete(
  "/drop/:token",
  getTokenFromQuery,
  validateToken,
  getUserById,
  dropApiKey
);

module.exports = router;

Now that we have created our models, routes and controllers, let's update our entry index.js file with the following code.

const { connectDB } = require("./config/db.config");
connectDB();
const morgan = require("morgan");
const createError = require("http-errors");
// routes
const notesRouter = require("./routes/notes.route");
const usersRouter = require("./routes/users.route");
const tokenRouter = require("./routes/apikeys.route");
const trashRouter = require("./routes/trash.route");

// accept JSON request
app.use(express.json());

// accept a form
app.use(
  express.urlencoded({
    extended: false,
  })
);

// throw an error for unavailable routes
app.use((req, res, next) => {
  return next(createError(404));
});

// log request info to the console
// @ts-ignore
app.use(morgan("dev"));

// set up routes
app.use("/notes", notesRouter);
app.use("/trash", trashRouter);
app.use("/account", usersRouter);
app.use("/tokens", tokenRouter);

Again let's update our package.json file.

"scripts": {
    "db:init": "node ./config/db.init.js"
    }

We initially started the server, but now we need to stop it so we can execute some commands, so in the terminal hit ctrl+c to stop the running server. Then run the following command,

npm run db:init

and restart the server

npm run dev

Testing the API.

Now that our API is ready, let's try it out. So in the rest folder, let's create some files. account.rest

### Create a new user
POST http://localhost:4420/account/sign-up
Content-Type: application/json

{
    "email":"mike@example.com",
    "password":"1234",
    "firstName":"mike",
    "lastName":"doe"
}

### login into an account, **user gets a new api key whenever they login**
POST http://localhost:4420/account/sign-in
Content-Type: application/json

{
    "email":"mike@example.com",
    "password":"1234"
}

Sample screenshots:

Screenshot 2022-05-06 190623.png

Screenshot 2022-05-06 190814.png

apikey.rest

### generate new api key
POST http://localhost:4420/tokens/new?api_key={API KEY HERE}

### revoke an api key
PUT http://localhost:4420/tokens/revoke/{THE API KEY YOU WANT TO REVOKE}?api_key={API KEY HERE}


### drop/delete an api key
DELETE http://localhost:4420/tokens/drop/{THE API KEY YOU WANT TO DROP}?api_key={API KEY HERE}

Sample screenshots:

Screenshot 2022-05-07 090212.png

Screenshot 2022-05-07 090406.png

Screenshot 2022-05-07 090832.png


notes.rest

### get all notes owned by a specific user
GET http://localhost:4420/notes?api_key={API KEY HERE}

### add new note
POST http://localhost:4420/notes/new?api_key={API KEY HERE}
Content-Type: application/json

{
"title":"a new note title ",
"body":"body of a new note"
}

### edit a note
PUT http://localhost:4420/notes/edit/{ NOTE ID }?api_key={API KEY HERE }
Content-Type: application/json

{
    "title":"an updated title",
    "body":"an updated body"
}

### move a note to trash, **this doesn't permanently delete the note**
PUT http://localhost:4420/notes/to-trash/{NOTE ID }?api_key={API KEY HERE }

### remove a note out from the trash,
PUT http://localhost:4420/notes/out-of-trash/{NOTE ID }?api_key={API KEY HERE }

Sample screenshots:

Screenshot 2022-05-06 191149.png Screenshot 2022-05-06 191408.png Screenshot 2022-05-06 235000.png Screenshot 2022-05-07 085836.png Screenshot 2022-05-07 085952.png

And trash.rest


### get notes in the trash
GET http://localhost:4420/trash?api_key={API KEY HERE}


### permanently delete a note IN Trash
DELETE http://localhost:4420/trash/{NOTE ID}?api_key={ API KEY HERE }

Conclusion

In this article, I have demonstrated how to build a Note Keeping API with NodeJS, Express, and HarperDB, if you find this article helpful, do give it a thumbs up 👍.

if there's any API you would love to see built with NodeJS and HarperDB, let me know in the comments or tweet me.


Sneak peek: In the next article, we are going to build an Image API.

 
Share this