Deno: Beyond Hello World - Drash Server

I was following Deno since it was announced by Ryan Dahl in 2018 at JSConf and its going to be in v1 in May 13 2020. I like Typescript and Node.js both. I started playing with it as soon as I found executable for Linux. They started Deno with Golang back then and moved to Rust but from users perspective there is nothing changed. You download executable and run your code. deno <file_name>.ts. Using Deno has many other benefits other than just having Typescript built in like no package.json so no node_modules folder, so directly download from url and run like in browser, only allow set of things like access to net or env or files to sandbox etc. There are many articles out there to give you these core concepts of Deno and why Ryan Dahl made it. I am linking video of introduction of Deno to world by Ryan Dahl.

As soon as I pickup Deno I wanted to build REST API with DB connection and deploy it but it was too early to do those. So, I abandoned it and moved to something else at that time.

Due to pandemic I got time to work upon things I wanted to work for so long and one of those things is Deno. Build a basic REST API with a database, password encryption and JWT. I found few options to build REST API on deno and starting with Drash - A REST microframework for Deno. I choose to go with postgres as its easier to setup in heroku also I couldn't find mongo driver for deno (if you know one please comment). Lets start

Basic Server with Drash

Drash uses class based HTTP Resources. If you have worked with django you will find very similar to it.

import Drash from "https://deno.land/x/drash@v0.37.1/mod.ts";

class HomeResource extends Drash.Http.Resource {
  static paths = ["/"];
  public GET() {
    this.response.body = "Hello World!";
    return this.response;
  }
}

const server = new Drash.Http.Server({
  address: `0.0.0.0:3000`,
  response_output: "application/json",
  resources: [HomeResource]
});

server.run();

Create a class with extends Drash Resource (Drash.Http.Resource). paths is an array of string where you add routes for this resource. Here it is HomeResoruce so I add root path. Then there are public methods which corresponds to HTTP methods like GET -> GET(), POST -> POST() etc. To send response you need to modify response body of resource. By default response status code will always be 200 OK until and unless changed.

Next is to create our server which can use out resources. Drash.Http.Server does that where you need to pass address (including port), type of response you will be sending like application/json or text/html and list of your resources then at last call server.run(). You can have multiple resources like that and add them in the resources list. Thats what I am going to do with adding postgres and jwt in 2 more resources - userResources, authResources.

Adding Database

Next step was to add database to our server and I choose to go with postgres and used deno_postgres. Also did some code re-structuring where I created a resource folder to keep all my resources and main file named a index.ts. After code re-structuring:

// resources/homeResource.ts
import Drash from "https://deno.land/x/drash@v0.37.1/mod.ts";

export default class HomeResource extends Drash.Http.Resource {
  static paths = ["/"];
  public GET() {
    this.response.body = "Hello World!";
    return this.response;
  }
}
// index.ts
import Drash from "https://deno.land/x/drash@v0.37.1/mod.ts";
import HomeResource from './resources/homeResource.ts';

const server = new Drash.Http.Server({
  address: `0.0.0.0:3000`,
  response_output: "application/json",
  resources: [HomeResource]
});

To add database added following lines after server.run()

// index.ts
// ... previous code...

import { Client } from "https://deno.land/x/postgres/mod.ts";

const denoPostgres = new Client({
  database: "deno_postgres",
  host: "localhost",
  port: "5432",
  user: "postgres", // specify your db user
  password: "postgres"
});

export {
  denoPostgres
}

But there is problem, deno_postgres is not a ORM and I need to check if tables are present or not, if not then create it when server starts. So, used denoPostgres.query() to check and create tables. Added following lines before export.

// index.ts
// ...previous code...

const createTables = async () => {
  console.log("Creating table User");
  try {
    await denoPostgres.connect();
    let userExists = await denoPostgres.query("SELECT table_catalog, table_schema FROM information_schema.tables WHERE table_name = 'users'");
    if (userExists.rows.length < 1) {
      let userTableQuery = `
        CREATE TABLE users(
          NAME VARCHAR (50) NOT NULL,
          EMAIL VARCHAR (50) NOT NULL,
          USERNAME VARCHAR (50) NOT NULL,
          PASSWORD VARCHAR (256) NOT NULL,
          PRIMARY KEY (USERNAME)
      );
      `;
      let userTable = await denoPostgres.query(userTableQuery);
      console.log('User table created');
    }
    await denoPostgres.end();
  } catch(e) {
    console.log("Error in db connection");
    console.log(e);
  }
}

createTables();

// ...export...

Adding user to database

I created a userResource.ts inside resources folder with following code

resources/userResource.ts

import Drash from "https://deno.land/x/drash@v0.37.1/mod.ts";

export default class UserResource extends Drash.Http.Resource {
  static paths = ["/user"];
  public async GET() {
    try {
      await denoPostgres.connect();
      let users = await denoPostgres.query(`SELECT * FROM users`);
      let returnObject: any = [];
      for(let i = 0; i < users.rows.length; i++) {
        const data = users.rows[i];
        let d: any = {};
        d['name'] = data[0]; 
        d['email'] = data[1];
        d['username'] = data[2];
        returnObject.push(d);
      }
      this.response.body = returnObject;
    } catch(e) {
      console.log(e);
    }
    return this.response;
  }
}

Here, when client hits endpoint <deno_server_resource>/user with GET method it just lists users present in database. Next I created a POST methods where body needs to have following

{
  "name": "John Doe",
  "email": "john.doe@example.com",
  "username': "johndoe",
  "password': "johndoe"
}

All are required and and password would be stored as encrypted, also it returns JWT token. So used 2 more libraries - deno-checksum and djwt.

Created a helper function to send bad request response

// helpers/errorResponse.ts

export function errorBadResponse(responseObject: any, message: string, error: boolean) {
  responseObject.body = {
    "error": error,
    "message": message
  }
  responseObject.status_code = 400;
  return responseObject;
}
// resources/userResource.ts

import Drash from "https://deno.land/x/drash@v0.37.1/mod.ts";
import { Hash, encode } from "https://deno.land/x/checksum/mod.ts";
import makeJwt, {
  Jose,
  Payload,
} from "https://deno.land/x/djwt/create.ts"
import { denoPostgres } from '../index.ts';
import { errorBadResponse } from '../helpers/errorResponse.ts';
const key = "supersecret";

export default class UserResource extends Drash.Http.Resource {
  // ...paths and GET method

  public async POST() {
    const name = this.request.getBodyParam("name");
    const email = this.request.getBodyParam("email");
    const username = this.request.getBodyParam("username");
    const password = this.request.getBodyParam("password");
    if (!name || !email || !username || !password) {
      return errorBadResponse(this.response, 'All fields are required', true);
    } 
    const encryptedPassword = new Hash("sha1").digest(encode(password)).hex().toString();
    await denoPostgres.connect();
    await denoPostgres.query(`INSERT INTO users VALUES ('${name}', '${email}', '${username}', '${encryptedPassword}')`);
    const header: Jose = {
      alg: "HS256",
      typ: "JWT"
    }
    const payload: Payload = {
      'name': name,
      'email': email,
      'username': username
    }
    const jwt = makeJwt({ header, payload }, key);
    this.response.body = {
      ...payload,
      "token": jwt
    }
    this.response.body = { name, email, username };
    return this.response;
  }
}

On successful registration it sends response as following:

{
  "name": "John Doe",
  "email": "john.doe@example.com",
  "username": "johndoe",
  "token": "<jwt_token>"
}

Next wanted to have a login route as <deno_server_resource>/auth as POST where you send following body and get same as above response is successful.

{
  "username": "johndoe",
  "password": "johndoe"
}
// resources/authResource.ts

import Drash from "https://deno.land/x/drash@v0.37.1/mod.ts";
import { Hash, encode } from "https://deno.land/x/checksum/mod.ts";
import makeJwt, {
  Jose,
  Payload,
} from "https://deno.land/x/djwt/create.ts"

const key = "supersecret"
import { denoPostgres } from '../index.ts';
import { errorBadResponse } from '../helpers/errorResponse.ts';

export default class AuthResource extends Drash.Http.Resource {
  static paths = ["/auth"];
  public async POST() {
    try {
      await denoPostgres.connect();
      const username = this.request.getBodyParam("username");
      const password = this.request.getBodyParam("password");
      if (!username || !password) {
        return errorBadResponse(this.response, 'All fields are required', true);
      }
      const query = `
        SELECT * FROM users WHERE username = '${username}'
      `;
      const result = await denoPostgres.query(query);
      if (!result.rows.length) {
        return errorBadResponse(this.response, 'No user present with given username', false);
      }
      const user = result.rows[0];
      const encryptedPassword = new Hash("sha1").digest(encode(password)).hex().toString();
      if (user[3] != encryptedPassword) {
        return errorBadResponse(this.response, 'Password is wrong', false);
      }
      const header: Jose = {
        alg: "HS256",
        typ: "JWT"
      }
      const payload: Payload = {
        'name': user[0],
        'email': user[1],
        'username': user[2]
      }
      const jwt = makeJwt({ header, payload }, key);
      this.response.body = {
        ...payload,
        "token": jwt
      }
      return this.response;
    } catch(e) {
      console.log(e);
    }
  }
}

Deploying

So basic PoC is ready now i want to deploy it to cloud service provider. So I thought to go with Heroku. Heroku doesn't have any official supported Deno buildpacks currently but developer community is always has solution. Deno buildpack for Heroku.

There are changes required for Heroku as I am using Heroku postgres also for database:

// index.ts

import Drash from "https://deno.land/x/drash@v0.37.1/mod.ts";

import HomeResource from './resources/homeResource.ts';
import UserResource from './resources/userResource.ts';
import authResource from './resources/authResource.ts';

// Take from env port that Heroku provides
const port = Deno.env()['PORT'] || 3000;

// Assign port to server
const server = new Drash.Http.Server({
  address: `0.0.0.0:${port}`,
  response_output: "application/json",
  resources: [HomeResource, UserResource, authResource]
});

server.run();

import { Client } from "https://deno.land/x/postgres/mod.ts";

// Get environment variables for postgres
const dbUrl = Deno.env()['DATABASE_URL'];

// Check whether env is present or use localhost
const denoPostgres = dbUrl ? new Client(dbUrl) : new Client({
  database: "deno_postgres",
  host: "localhost",
  port: "5432",
  user: "postgres", // specify your db user
  password: "postgres"
});

const createTables = async () => {
  console.log("Creating table User");
  await denoPostgres.connect();
  try {
    let userExists = await denoPostgres.query("SELECT table_catalog, table_schema FROM information_schema.tables WHERE table_name = 'users'");
    if (userExists.rows.length < 1) {
      let userTableQuery = `
        CREATE TABLE users(
          NAME VARCHAR (50) NOT NULL,
          EMAIL VARCHAR (50) NOT NULL,
          USERNAME VARCHAR (50) NOT NULL,
          PASSWORD VARCHAR (256) NOT NULL,
          PRIMARY KEY (USERNAME)
      );
      `;
      let userTable = await denoPostgres.query(userTableQuery);
      console.log('User table created');
    }
    await denoPostgres.end();
  } catch(e) {
    console.log(e);
  }
}

createTables();

export {
  denoPostgres
}

We need to add two files runtime.txt and as usual Procfile for Heroku.

// runtime.txt
v0.38.0
// Procfile
web: deno --allow-net --allow-env index.ts

Now everything is ready to deploy and as usual do git push heroku to deploy and you basic Deno server is running on cloud.

So, there are many things that needs to be done in this server but Deno is in rapid development and many libraries are coming and many are being ported from Node to Deno. There are many other libraries for Deno which I will try and deploy and write article.

Hope you like this article.

Stay Safe! Stay Home!

Happy Learning! Happy Coding!

No Comments Yet