Dino: Build a Rest API with JWT

On the eve of the start of the course “Node.JS Developer” we invite everyone to watch an open lesson on the topic “Dockerizing Node.js Applications”

And now we are sharing the traditional translation of useful material. Enjoy reading.


Since the first version, Deno has become a buzzword for Javascript / TypeScript / Node. Let’s dive into this technology by creating a JWT-secured REST API.

It is advisable to already have some basics in Node and its ecosystem (Express, Nodemon, Sequelize, etc.) in order to follow this tutorial.

What is Dino (Deno)?

Deno is a simple, modern and secure JavaScript and TypeScript runtime that uses V8 and is built into Rust.

There are already many articles detailing this topic, so I will not dwell on it. I can recommend this

Introduction

Since the official release V1Deno has become a buzzword in a matter of weeks (for the fan, here is the popularity curve for “deno” searches on Google).

What can you do with a secure runtime for Typescript and Javascript “?

To better understand and give my opinion on this growing project, I decided to create a secure JWT REST API and share my feelings with you.

I am used to working with Node.js and Express

goal

The goal of this tutorial will be to create a secure REST API, which means:

  • Server Tuning

  • Creating a model with ORM and database

  • CRUD user

  • Implementing secure routing with JWT

Premise

To create our REST API with JWT, I will use:

  • Deno (I recommend the official documentation for installation: here)

  • VSCode and Deno support plugin available from link

And also the following packages (I’ll come back to this throughout the tutorial):

Installation

First, let’s set up the project structure to provide specific guidance for creating a clean and “production-ready” project.

|-- DenoRestJwt
    |-- controllers/
    |   |-- database/
    |   |-- models/
    |-- helpers/
    |-- middlewares/
    |-- routers/
    |-- app.ts

If we were on a Node + Express application, I would use Nodemon to ease development, Nodemon restarts the server automatically after changes in the code.

Nodemon is a tool that helps you develop node.js based applications by automatically restarting your Node application when it detects changes to a file in a directory.

To keep the same “development comfort”, I decided to use Denon, its counterpart for Deno.

deno install --allow-read --allow-run --allow-write -f --unstable 
https://deno.land/x/denon/denon.ts

Let’s tweak the Denon configuration a bit. This will be useful later (especially for managing environment variables).

// into denon.json
{
  "$schema": "https://deno.land/x/denon/schema.json",
  "env": {},
  "scripts": {
    "start": {
      "cmd": "deno run app.ts"
    }
  }
}

We are now ready to start coding in good conditions! To start Denon, just type in the console denon start:

➜ denon start
[denon] v2.0.2
[denon] watching path(s): *.*
[denon] watching extensions: ts,js,json
[denon] starting `deno run app.ts`
Compile file:///deno-crashtest/app.ts
[denon] clean exit - waiting for changes before restart

You can see that our server is running … but it crashes! This is ok, it has no code to execute in app.ts

Let’s initialize our server

I decided to use the framework Oak

Oak is a middleware framework for Deno http server including router middleware. This middleware framework is inspired by Koa and middleware is inspired by @koa/router

Let’s initialize our server with Oak:

// app.ts
import { Application, Router, Status } from "https://deno.land/x/oak/mod.ts";

// Initialise app
const app = new Application();

// Initialise router
const router = new Router();

// Create first default route
router.get("https://habr.com/", (ctx) => {
    ctx.response.status = Status.OK;
    ctx.response.body = { message: "It's work !" };
});

app.use(router.routes());
app.use(router.allowedMethods());

console.log("? Deno start !");
await app.listen("0.0.0.0:3001");

Now if we start our server with denon start

error: Uncaught PermissionDenied: network access to "0.0.0.0:3001", 
run again with the --allow-net flag

This is one of the big differences between Deno and Node: Deno is secure by default and does not have access to network… You must authorize it:

// into denon.json
"scripts": {
    "start": {
      // add --allow-net
      "cmd": "deno run --allow-net app.ts"
    }
  }

You can now access from your browser (although I recommend using Postman) to localhost: 3001 :

{
    "message": "It's work !"
}

Installing the database

I’ll use DenoDB as an ORM (in particular because it supports Sqlite3). Moreover, it is very similar to Sequelize (to which I’m used to).

Let’s add the first controller Database and the file Sqlite3.

|-- DenoRestJwt
    |-- controllers/
	|   |-- Database.ts
        |   |-- database/
	|   |   |-- db.sqlite
    |   |-- models/
    |-- app.ts
// Database.ts
import { Database } from "https://deno.land/x/denodb/mod.ts";

export class DatabaseController {
  client: Database;

	/**
   * Initialise database client
   */
  constructor() {
    this.client = new Database("sqlite3", {
      filepath: Deno.realPathSync("./controllers/database/db.sqlite"),
    });
  }

  /**
   * Initialise models
   */
  async initModels() {
    this.client.link([]);
    await this.client.sync({});
  }
}

Our ORM is initialized. You can notice that I am using realPathSyncwhich requires additional permission. Let’s add --allow-read unfinished and --allow-write unfinished in denon.json:

"scripts": {
    "start": {
      "cmd": "deno run --allow-write --allow-read --allow-net app.ts"
    }
  }

All that’s left to do is create a user model through our ORM:

|-- DenoRestJwt
    |-- controllers/
    |   |-- models/
    |       |-- User.ts
    |-- app.ts
// User.ts
import { Model, DATA_TYPES } from "https://deno.land/x/denodb/mod.ts";
import nanoid from "https://deno.land/x/nanoid/mod.ts";

export interface IUser {
  id?: string;
  firstName: string;
  lastName: string;
  password: string;
}

export class User extends Model {
  static table = "users";
  static timestamps = true;
  
  static fields = {
    id: {
      primaryKey: true,
      type: DATA_TYPES.STRING,
    },
    firstName: {
      type: DATA_TYPES.STRING,
    },
    lastName: {
      type: DATA_TYPES.STRING,
    },
    password: {
      type: DATA_TYPES.TEXT,
    },
  };
  
	// Id will generate a nanoid by default
  static defaults = {
    id: nanoid(),
  };
}

There is nothing new here, so I will not dwell on this. (ps: I am using nanoid for managing my UUID, I will let you read this very interesting article about it).

I am taking this opportunity to add a function that will be useful later: a password hash. For this I use Bcrypt:

// inside User's class
import * as bcrypt from "https://deno.land/x/bcrypt/mod.ts";

// ...
static async hashPassword(password: string) {
    const salt = await bcrypt.genSalt(8);
    return bcrypt.hash(password, salt);
}

Finally, let’s link our model to our ORM:

// Database.ts
import { User } from "./models/User.ts";

export class DatabaseController {
//...
  initModels() {
      // Add User here
      this.client.link([User]);
      return this.client.sync({});
  }
}

Good! Now that our server and database are in place, it’s time to initialize the account creation routes …

User controller

There is nothing more basic than a good CRUD:

|-- DenoRestJwt
    |-- controllers/
    |   |-- Database.ts
    |   |-- UserController.ts
import * as bcrypt from "https://deno.land/x/bcrypt/mod.ts";
import { IUser, User } from "./models/index.ts";

export class UserController {
  async create(values: IUser) {
    // Call static user method
    const password = await User.hashPassword(values.password);

    const user: IUser = {
      firstName: values.firstName,
      lastName: values.lastName,
      password,
    };

    await User.create(user as any);

    return values;
  }
  async delete(id: string) {
    await User.deleteById(id);
  }

  getAll() {
    return User.all();
  }

  getOne(id: string) {
    return User.where("id", id).first();
  }

  async update(id: string, values: IUser) {
    await User.where("id", id).update(values as any);
    return this.getOne(id);
  }

  async login(lastName: string, password: string) {
    const user = await User.where("lastName", lastName).first();
    if (!user || !(await bcrypt.compare(password, user.password))) {
      return false;
    }

    // TODO generate JWT

  }
}

I am just using the methods provided by the ORM. Now we just have to manage the generation of the JWT.

Setting up routing

Now it’s time to create our various paths and call our freshly encoded controller.

|-- DenoRestJwt
    |-- routers
        |-- UserRoute.ts
import { Router, Status } from "https://deno.land/x/oak/mod.ts";
import { UserController } from "../controllers/UserController.ts";
import { BadRequest } from "../helpers/BadRequest.ts";
import { NotFound } from "../helpers/NotFound.ts";

// instantiate our controller
const controller = new UserController();

export function UserRoutes(router: Router) {
  return router
    .get("/users", async (ctx) => {
      const users = await controller.getAll();

      if (users) {
        ctx.response.status = Status.OK;
        ctx.response.body = users;

      } else {
        ctx.response.status = Status.NotFound;
        ctx.response.body = [];
      }

      return;
    })
    .post("/login", async (ctx) => {
      if (!ctx.request.hasBody) {
        return BadRequest(ctx);
      }
      const { value } = await ctx.request.body();

      // TODO generate JWT

      ctx.response.status = Status.OK;
      ctx.response.body = { jwt };
    })
    .get("/user/:id", async (ctx) => {
      if (!ctx.params.id) {
        return BadRequest(ctx);
      }

      const user = await controller.getOne(ctx.params.id);
      if (user) {
        ctx.response.status = Status.OK;
        ctx.response.body = user;
        return;
      }

      return NotFound(ctx);
    })
    .post("/user", async (ctx) => {
      if (!ctx.request.hasBody) {
        return BadRequest(ctx);
      }

      const { value } = await ctx.request.body();
      const user = await controller.create(value);

      if (user) {
        ctx.response.status = Status.OK;
        ctx.response.body = user;
        return;
      }

      return NotFound(ctx);
    })
    .patch("/user/:id", async (ctx) => {
      if (!ctx.request.hasBody || !ctx.params.id) {
        return BadRequest(ctx);
      }

      const { value } = await ctx.request.body();
      const user = await controller.update(ctx.params.id, value);

      if (user) {
        ctx.response.status = Status.OK;
        ctx.response.body = user;
        return;
      }

      return NotFound(ctx);
    })
    .delete("/user/:id", async (ctx) => {
      if (!ctx.params.id) {
        return BadRequest(ctx);
      }

      await controller.delete(ctx.params.id);

      ctx.response.status = Status.OK;
      ctx.response.body = { message: "Ok" };
    });
}

All we need to do is call our logic from our controller.

I am using HTTP methods to clearly separate routes. I also created helpers to manage the returned errors. The sources can be found directly from the GitHub project! All we have to do is call our router in our application:

// app.ts
import { DatabaseController } from "./controllers/Database.ts";
import { UserRoutes } from "./routers/UserRoute.ts";

const userRoutes = UserRoutes(router);
app.use(userRoutes.routes());
app.use(userRoutes.allowedMethods());

await new DatabaseController().initModels();

Security and JWT

It’s time to add security to this project! I use JWT for this.

1. Create a secure route

First of all, we are going to install the middle layer:

  • Checks if the “Authorization” header exists in the request.

  • Pulls out the title

  • Validates the title

  • Returns an error / Accepts the request and calls the private route

I will use the library Djwt

|-- DenoRestJwt
    |-- middlewares/
    |   |-- jwt.ts

Our function will have to accept the request context in the parameter, extract the token from the headers, check its validity and act accordingly.

import { Context, Status } from "https://deno.land/x/oak/mod.ts";
import { validateJwt } from "https://deno.land/x/djwt/validate.ts";

/**
 * Create a default configuration
 */
export const JwtConfig = {
  header: "Authorization",
  schema: "Bearer",
	// use Env variable
  secretKey: Deno.env.get("SECRET") || "",
  expirationTime: 60000,
  type: "JWT",
  alg: "HS256",
};

export async function jwtAuth(
  ctx: Context<Record<string, any>>,
  next: () => Promise<void>
) {
    // Get the token from the request
    const token = ctx.request.headers
      .get(JwtConfig.header)
      ?.replace(`${JwtConfig.schema} `, "");
    
    // reject request if token was not provide
    if (!token) {
      ctx.response.status = Status.Unauthorized;
      ctx.response.body = { message: "Unauthorized" };
      return;
    }
    
    // check the validity of the token
    if (
      !(await validateJwt(token, JwtConfig.secretKey, { isThrowing: false }))
    ) {
      ctx.response.status = Status.Unauthorized;
      ctx.response.body = { message: "Wrong Token" };
      return;
    }
    
    // JWT is correct, so continue and call the private route
    next();
  }

Please note that we need a secret key in order to encrypt our token. I use Deno environment variables for this. So we need to make a few changes to Denon’s configuration: add our variable and let Deno receive environment variables.

{
  "$schema": "<https://deno.land/x/denon/schema.json>",
  // Add env variable
  "env": {
    "SECRET": "ADRIEN_IS_THE_BEST_AUTHOR_ON_MEDIUM"
  },
  "scripts": {
    "start": {
      // add the permission with --allow-env
      "cmd": "deno run --allow-env --allow-read --allow-net app.ts"
    }
  }
}

(ps: if you want secure environment variables, i recommend this tutorial)

Then let’s create our private route.

|-- DenoRestJwt
    |-- routers
        |-- UserRoute.ts
        |-- PrivateRoute.ts

Just call our method before calling our route:

import { Router, Status } from "https://deno.land/x/oak/mod.ts";
import { jwtAuth } from "../middlewares/jwt.ts";

export function PrivateRoutes(router: Router) {
  // call our middleware before our private route
  return router.get("/private", jwtAuth, async (ctx) => {
    ctx.response.status = Status.OK;
    ctx.response.body = { message: "Conntected !" };
  });
}

Don’t forget to add it to our application:

import { Router, Status } from "https://deno.land/x/oak/mod.ts";
import { jwtAuth } from "../middlewares/jwt.ts";

export function PrivateRoutes(router: Router) {
  // call our middleware before our private route
  return router.get("/private", jwtAuth, async (ctx) => {
    ctx.response.status = Status.OK;
    ctx.response.body = { message: "Conntected !" };
  });
}

If we try to call our API on / private, we get the correct answer:

{
    "message": "Unauthorized"
}

2. JWT generation

Now it’s time to set up token generation when users log in. Remember, we left // TODO generate JWT in our controller. Before completing it, we’ll first add a static method to our User model to generate a token.

// User.ts
import {
  makeJwt,
  setExpiration,
  Jose,
  Payload,
} from "https://deno.land/x/djwt/create.ts";
import { JwtConfig } from "../../middlewares/jwt.ts";
// ...

export class User extends Model {
// ...
	static generateJwt(id: string) {
	    // Create the payload with the expiration date (token have an expiry date) and the id of current user (you can add that you want)
	    const payload: Payload = {
	      id,
	      exp: setExpiration(new Date().getTime() + JwtConfig.expirationTime),
	    };
	    const header: Jose = {
	      alg: JwtConfig.alg as Jose["alg"],
	      typ: JwtConfig.type,
	    };

	    // return the generated token
	    return makeJwt({ header, payload, key: JwtConfig.secretKey });
	  }
	// ...
}

Let’s call this method in our controller:

// UserController.ts

export class UserController {
// ...
	async login(lastName: string, password: string) {
			const user = await User.where("lastName", lastName).first();
			if (!user || !(await bcrypt.compare(password, user.password))) {
				return false;
			}
			// Call our new static method
			return User.generateJwt(user.id);
	 }
}

Finally, let’s add this logic to our router:

// UserRoute.ts

// ...
.post("/login", async (ctx) => {
      if (!ctx.request.hasBody) {
        return BadRequest(ctx);
      }
      const { value } = await ctx.request.body();
			
      // generate jwt
      const jwt = await controller.login(value.lastName, value.password);
      if (!jwt) {
        return BadRequest(ctx);
      }

      ctx.response.status = Status.OK;
      // and return it
      ctx.response.body = { jwt };
    })
// ...

Now, if we try to connect, we have:

// localhost:3001/login
{
    "jwt": 
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IlEyY0ZZcUxKWk5Hc0toN0FWV0hzUiIsImV4cCI6MTU5MDg0NDU2MDM5MH0.drQ3ay5_DYuXEOnH2Z0RKbhq9nZElWCMvmypjI4BjIk"
}

(Don’t forget to create an account earlier)

Let’s add this token to our headers Authorization and call our private route again:

// localhost:3001/private with token in headers
{
    "message": "Connected !"
}

Great! Is there our secure API?

You can find this project on my Github: here (I am adding postman collection to make a request).

My impressions of Deno

I decided to share with you my impressions of Depo, which will give you some idea of ​​it:

Importing modules by URL at the beginning is a bit counter-intuitive: you always want to do npm i or yarn add… Moreover, we have to run Deno to cache our imports, and only then do we have access to auto-completion.

The remote module XXX has not been cached
  • I always use TypeScript in my Javascript projects, so I didn’t get lost in the beginning. On the contrary, I am quite familiar with him.

  • Interesting point: permissions. I think it’s good that Deno, for example, requires permissions to access the network. This makes us, as developers, aware of the access and rights of our program. (more safely)

  • At first, we got a little confused about where to look for packages (https://deno.land/x -> 460 packages and NPM -> + 1 million).

  • You can never be sure if a package also works for Deno or not. You + always want to be closer to what you know and use in Node in order to port it to Deno. I don’t know if this is good or bad, it’s still javascript …


Learn more about the course “Node.JS Developer”

View an open lesson on the topic “Dockerizing Node.js Applications”


GET A DISCOUNT

Similar Posts

Leave a Reply