Create a server with deno and mongo.

Despite the success of Node.js and the great effort done by the community since its creation, Ryan Dahl the creator of the famous javascript runtime, decided however in 2018 to design a new secure runtime for javascript built on V8, Rust, Typescript and Tokio (event loop). He declared that there are some design mistakes in Node.js and he regrets about them, then he has created deno which takes into consideration those anomalies. If you are curious about the issue, you can check his presentation in JSConf EU conference in 2018.

The purpose of this article is:

  • To create an api to manage employees.
  • To create environment variables using denv.
  • To implement controllers for the api.
  • To communicate with a database (mongodb) using deno_mongo.
  • To use a simple deno framework called abc.

First of all, you need to install deno in your machine, and according to your OS you can choose the suitable command line, check install section for more information.

PS: At the moment of writing this article, we use deno v1.0.0 to create our api.

Let's launch work

In order to facilitate the design of our server, we need a framework (let's say the equivalent of express in Node.js). In our case, we choose abc a simple deno framework to create web applications (abc is not the only framework there are others like alosaur, espresso, fen, oak, etc...).

First of all, we start by decalring our environment variables in .env file:

DB_NAME=deno_demo
DB_HOST_URL=mongodb://localhost:27017

Then, we create an error middleware to handle errors catched in the controllers:

import { MiddlewareFunc } from "https://deno.land/x/abc@v1.0.0-rc2/mod.ts";
export class ErrorHandler extends Error {
  status: number;
  constructor(message: string, status: number) {
    super(message);
    this.status = status;
  }
}
export const ErrorMiddleware: MiddlewareFunc = (next) =>
  async (c) => {
    try {
      await next(c);
    } catch (err) {
      const error = err as ErrorHandler;
      c.response.status = error.status || 500;
      c.response.body = error.message;
    }
  };

Then, we create server.ts (the main file of our server):

import { Application } from "https://deno.land/x/abc@v1.0.0-rc2/mod.ts";
import "https://deno.land/x/denv/mod.ts";
import {
  fetchAllEmployees,
  createEmployee,
  fetchOneEmployee,
  updateEmployee,
  deleteEmployee,
} from "./controllers/employees.ts";
import { ErrorMiddleware } from "./utils/middlewares.ts";

const app = new Application();

app.use(ErrorMiddleware);

app.get("/employees", fetchAllEmployees)
  .post("/employees", createEmployee)
  .get("/employees/:id", fetchOneEmployee)
  .put("/employees/:id", updateEmployee)
  .delete("/employees/:id", deleteEmployee)
  .start({ port: 5000 });

console.log(`server listening on http://localhost:5000`);

In the first line, you will find that we import modules directly from the internet using an url. The first time you need to import a module, deno fetches it then download cache the dependencies.

The second line calls denv in order to load the enviornment variables from the .env file.

The rest of code is almost similar to express, nothing special.

Now, we need to configure our database to interact with the server. Fortunately, there is deno_mongo a MongoDB database driver developed for deno. It is under construction and still does not contain the different methods of mongodb driver but it is ok for a simple demo.

import { init, MongoClient } from "https://deno.land/x/mongo@v0.6.0/mod.ts";

// Initialize the plugin
await init()

class DB {
  public client: MongoClient;
  constructor(public dbName: string, public url: string) {
    this.dbName = dbName;
    this.url = url;
    this.client = {} as MongoClient;
  }
  connect() {
    const client = new MongoClient();
    client.connectWithUri(this.url);
    this.client = client;
  }
  get getDatabase() {
    return this.client.database(this.dbName);
  }
}

const dbName = Deno.env.get("DB_NAME") || "deno_demo";
const dbHostUrl = Deno.env.get("DB_HOST_URL") || "mongodb://localhost:27017";
const db = new DB(dbName, dbHostUrl);
db.connect();

export default db;

We create a DB class which allows to start a database connection, so we create a new instance with DB_NAME and DB_HOST_URL.

Then, we write our controllers, let's start by createEmployee:

import { HandlerFunc, Context } from "https://deno.land/x/abc@v1.0.0-rc2/mod.ts";
import db from '../config/db.ts';

const database = db.getDatabase;
const employees = database.collection('employees');

interface Employee {
  _id: {
    $oid: string;
  };
  name: string;
  age: number;
  salary: number;
}

export const createEmployee: HandlerFunc = async (c: Context) => {
  try {
    if (c.request.headers.get("content-type") !== "application/json") {
      throw new ErrorHandler("Invalid body", 422);
    }
    const body = await (c.body());
    if (!Object.keys(body).length) {
      throw new ErrorHandler("Request body can not be empty!", 400);
    }
    const { name, salary, age } = body;

    const insertedEmployee = await employees.insertOne({
      name,
      age,
      salary,
    });

    return c.json(insertedEmployee, 201);
  } catch (error) {
    throw new ErrorHandler(error.message, error.status || 500);
  }
};

The mongo driver returns an object containing only the $oid attribute (I hope it will be updated in the next versions of the module).

To fetch all the employees, we call:

export const fetchAllEmployees: HandlerFunc = async (c: Context) => {
  try {
    const fetchedEmployees: Employee[] = await employees.find();

    if (fetchedEmployees) {
      const list = fetchedEmployees.length
        ? fetchedEmployees.map((employee) => {
          const { _id: { $oid }, name, age, salary } = employee;
          return { id: $oid, name, age, salary };
        })
        : [];
      return c.json(list, 200);
    }
  } catch (error) {
    throw new ErrorHandler(error.message, error.status || 500);
  }
};

For fetching a given employee by id, fetchOneEmployee will be called:

export const fetchOneEmployee: HandlerFunc = async (c: Context) => {
  try {
    const { id } = c.params as { id: string };

    const fetchedEmployee = await employees.findOne({ _id: { "$oid": id } });

    if (fetchedEmployee) {
      const { _id: { $oid }, name, age, salary } = fetchedEmployee;
      return c.json({ id: $oid, name, age, salary }, 200);
    }

    throw new ErrorHandler("Employee not found", 404);
  } catch (error) {
    throw new ErrorHandler(error.message, error.status || 500);
  }
};

Update a given employee:

export const updateEmployee: HandlerFunc = async (c: Context) => {
  try {
    const { id } = c.params as { id: string };
    if (c.request.headers.get("content-type") !== "application/json") {
      throw new ErrorHandler("Invalid body", 422);
    }

    const body = await (c.body()) as {
      name?: string;
      salary: string;
      age?: string;
    };

    if (!Object.keys(body).length) {
      throw new ErrorHandler("Request body can not be empty!", 400);
    }

    const fetchedEmployee = await employees.findOne({ _id: { "$oid": id } });

    if (fetchedEmployee) {
      const { matchedCount } = await employees.updateOne(
        { _id: { "$oid": id } },
        { $set: body },
      );
      if (matchedCount) {
        return c.string("Employee updated successfully!", 204);
      }
      return c.string("Unable to update employee");
    }
    throw new ErrorHandler("Employee not found", 404);
  } catch (error) {
    throw new ErrorHandler(error.message, error.status || 500);
  }
};

The driver here returns an object containing:

  • matchedCount
  • modifiedCount
  • upsertedId

Finally, to delete an employe:

export const deleteEmployee: HandlerFunc = async (c: Context) => {
  try {
    const { id } = c.params as { id: string };

    const fetchedEmployee = await employees.findOne({ _id: { "$oid": id } });

    if (fetchedEmployee) {
      const deleteCount = await employees.deleteOne({ _id: { "$oid": id } });
      if (deleteCount) {
        return c.string("Employee deleted successfully!", 204);
      }
      throw new ErrorHandler("Unable to delete employee", 400);
    }

    throw new ErrorHandler("Employee not found", 404);
  } catch (error) {
    throw new ErrorHandler(error.message, error.status || 500);
  }
};

Let's start our server now:

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

In order, to guarantee a secure exection of the program, deno blocks every access to disk, networks or environment variables. Therefore, to allow the server to be executed, you need to add the following flags:

  • --allow-write
  • --allow-read
  • --allow-plugin
  • --allow-net
  • --allow-env

Probably, you will ask yourself " how will I know which flags I have to add to execute the server?". Don't worry you will get a messge in the console log asking you to add a given flag.

Now, you will see something similar to this in your terminal:

INFO load deno plugin "deno_mongo" from local "~/.deno_plugins/deno_mongo_40ee79e739a57022e3984775fe5fd0ff.dll"
server listening on http://localhost:5000

Summary

In this article, we :

  • Created an employees' api using Deno.
  • Created a connection to a mongodb database using mongo driver for deno.
  • Used the abc framework to create our server.
  • Declared the environment variables using denv.

You probably realized that we :

  • Don't need to initialize a package.json file or install modules under node_modules.
  • Import modules directly using urls.
  • Add flags to secure the execution of the program.
  • Don't install typescript locally because it's compiled in Deno.

That's it folks, don't hesitate to leave a comment if there is any mistake, if you have a question or a suggestion. If you like the article, don't forget to click the heart button or tweet it ;).

Code

You can find the code here: github.com/slim-hmidi/deno-employees-api

References

No Comments Yet