Create a simple RESTful API with Deno & MongoDB - Pt.2

In order to follow this tutorial you need a running MongoDB instance on your computer or in the cloud. For more information about how to setup MongoDB on different operating systems you can checkout https://docs.mongodb.com/manual/tutorial/.

At this point in the tutorial series we have a simple GET request to the url '/user' in our router. Because we are going to expand this with multiple routes with more logic we now want to seperate the logic from the router. To do so we create a folder called "controllers" and add a file user.ts where the logic for our requests is stored. We add a function for our GET request with the same behaviour as before and export an instance of the class as default so we can access it in our router:

import { RouterContext } from "./../deps.ts";

export class UserController {
  getById(ctx: RouterContext) {
    ctx.response.body = 'User1';
  }
}

export default new UserController();

In the router we then need to import the userController and update the GET request to use the controller's function:

import { Router} from "./deps.ts";
import userController from "./controllers/user.ts";

const router = new Router();

router.get('/user', userController.getById);

export default router;

To test if everything is still working correctly we can now make a GET request to http://localhost:8000/user again and should get 'User1' as a response.

Before we add a few more routes like register and login to give our application the basic functionality it needs for user management, we will connect our application to a database. We use MongoDB as the database and the [deno_mongo](https://deno.land/x/mongo package as a database driver. Additionally we add the dotenv package so we can use environment variables in our code and outsource our database credentials.

To use deno_mongo and dotenv we need to import and export them in our deps.ts file:

export { MongoClient } from "https://deno.land/x/mongo@v0.11.1/mod.ts";
import "https://deno.land/x/dotenv@v0.5.0/load.ts";

For the environment variables we create a new file called .env in the root folder and specify the URI and a database name:

# MongoDB connect URI
MONGODB_URI = mongodb://localhost:27017
# MondoDB database name
DB_NAME = deno_tutorial

We then create a new file called db.ts in the root folder to setup the Mongo client and connect our application to the database. We create a new instance of the MongoClient and connect to the URI speficied in the environment variables. We similarly specify the database name and export it together with a database collection we call "users", where all of our users should be stored:

import { MongoClient } from "./deps.ts";

const client = new MongoClient();
client.connectWithUri(Deno.env.get("MONGODB_URI") || "");

const db = client.database(Deno.env.get("DB_NAME") || "");
export default db;

export const userCollection = db.collection("users");

We now have a userCollection and a route to get a single user. Before we connect them and add the logic to the route we will add a user model to our application. Therefore we create a new folder called models and a file called user.ts. We export a class User as default and add some variables for things like id, name, email and password we want our user model to hold.

import { userCollection } from "../db.ts";

export default class User {

  public id: string = "";
  public name: string = "";
  public email: string = "";
  public password: string = "";

  constructor({ id = "", name = "", email = "", password = "" }) {
    this.id = id;
    this.name = name;
    this.email = email;
    this.password = password;
  }
}

We also want to add methods for saving a user to the database and a method to check if a user already exists. But first let's have a look at how MongoDB will store our user data in the collection. Besides name, email and password there is a parameter_id, which contains an ObjectID and not an id as string, which we would like to have:

{
   "_id": {
      "$oid": ""
   },
   "name": "",
   "email": "",
   "password": ""
}

To convert this ObjectId to the id we want as a string, we create a little helper class. In a new folder called helpers we create a file called db.ts and export a class called DBUtils with a method called convertID, which takes the user object as an input and converts the ObjectId to a string:

export default class DbUtils {
  public static convertId(data: any) {
    data.id = data._id.$oid;
    delete data._id;
    return data;
  }
}

In our user model we can now import the DbUtils class. Then we add a method findOne with some parameters which gets a user from MongoDB's userCollection to check if a user already exists. If it doesn't exist we simply return null and if it does, we return the user with the converted id. We also add a method called save to insert a user into the database collection. Therefore we delete the id first, because MongoDB generates an ObjectID in the format we looked at above. Then we insert the user into the user collection and convert the ObjectID, we get back, to an id with the type string. Finally we return the whole user object so we can use it in our controller:

import DbUtils from "../helpers/db.ts";

[...]

static async findOne(params: object): Promise<User | null> {
    const result = await userCollection.findOne(params);
    if (!result) {
      return null;
    }
    return DbUtils.convertId(result);
  }

  async save() {
    delete this.id;
    const { $oid } = await userCollection.insertOne(this);
    this.id = $oid;
    return this;
  }

With the user model completed for now we can update and add the methods in the userController. We start with the register function where we get the user's parameters through the RouterContext's request body and call the model's findOne method to check if a user with that email already exists. If that's the case we output an error message and a status code of 422 and, if not, we call the model's save method to put that user into the database collection and return the user's credentials without the password and a status code of 201 to check if it worked correctly. The user controller should now look like this:

import { RouterContext } from "../deps.ts";
import User from "../models/user.ts";

export class UserController {
  get(ctx: RouterContext) {
    ctx.response.body = 'User1';
  }

  async register(ctx: RouterContext) {
    const { name, email, password } = await ctx.request.body().value;

    let user = await User.findOne({ email });
    if (user) {
      ctx.response.status = 422;
      ctx.response.body = { message: "Email is already in use" };
      return;
    }

    user = new User({ name, email, password });
    await user.save();
    ctx.response.status = 201;
    ctx.response.body = {
      id: user.id,
      name: user.name,
      email: user.email
    };
  }
}

export default new UserController();

To finish things off we can now add a new route for the register method to the router.ts file which now looks like this:

import { Router } from "./deps.ts";
import userController from "./controllers/user.ts";

const router = new Router();

router
  .get("/user", userController.get)
  .post("/register", userController.register);
export default router;

To test our newly created register method we need to restart the application. Because we added the deno_mongo and the dotenv package we need to add some more permissions when starting our application:

deno run --allow-net --allow-write --allow-read --allow-env --allow-plugin --unstable server.ts

While the GET request to http://localhost:8000/user should still return "User1", we can now make a POST request to http://localhost:8000/register. We need to specify the content-type as application/json and send a name, an email and a password

content-type: application/json

{
    "name": "User2",
    "email": "user2@marcomaisel.de",
    "password": "test123"
}

and should get a response like this:

HTTP/1.1 201 Created
content-length: 79
content-type: application/json; charset=utf-8

{
  "id": "5f491041005267f000c17615",
  "name": "User2",
  "email": "user2@marcomaisel.de"
}

That's it for part two. We added another route for user registration and set up the connection to a MongoDB while adding a user model, user controller and a helper class for the database.

The header image is by Dimitrij Aga and can be found at https://deno.land/artwork