Dino (Deno): Create Rest API with JWT

In anticipation of the start of the "Node.JS Developer" course , we invite everyone to watch the open lesson on the topic "Dockerization of 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.





, , . .





V1, Deno " " ( , "deno" Google).





Typescript and Javascript"?





, JWT REST API .





Node.js Express.





REST API, :









  • ORM





  • CRUD-





  • JWT





REST API JWT, :





  • Deno ( : )





  • VSCode Deno,





( ):





  • Denon





  • Oak





  • Djwt





  • Denodb





  • Bcrypt 





-, , " " .





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



Node + Express , Nodemon , Nodemon .





Nodemon - , node.js, Node .





" ", Denon, Deno.





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



Denon. ( ).





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



! Denon, 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
      
      



, … ! , app.ts



.





 Oak.





Oak - http- Deno, . Koa, @koa/router



.





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("/", (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");
      
      



, denon start



.





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



Deno Node: Deno network



. :





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



( Postman) localhost:3001 :





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



DenoDB ORM ( , Sqlite3). , Sequelize ( ).





Database



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({});
  }
}
      
      



ORM . , realPathSync



, . --allow-read



--allow-write



denon.json



:





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



, , 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(),
  };
}
      
      



, . (ps: nanoid



UUID, ).





, , : . 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);
}
      
      



, ORM :





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

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



! , , … 





User controller

, 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

  }
}
      
      



, ORM. JWT.





.





|-- 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" };
    });
}
      
      



, , .





HTTP . . GitHub! , , :





// 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();
      
      



JWT

! JWT .





1.

, :





  • , "Authorization".









  •  





  • /





Djwt.





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



, , .





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();
  }
      
      



, , . Deno. Denon: Deno .





{
  "$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: , )





.





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



:





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 !" };
  });
}
      
      



:





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 !" };
  });
}
      
      



API /private , :





{
    "message": "Unauthorized"
}
      
      



2.  JWT

. , // TODO generate JWT . User, .





// 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 });
	  }
	// ...
}
      
      



:





// 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);
	 }
}
      
      



, :





// 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 };
    })
// ...
      
      



, , :





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



( )





Authorization



:





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



! API ?.





Github: ( , ).





Deno

Depo, :





URL - : npm i



 yarn add



. , Deno, , .





The remote module XXX has not been cached
      
      



  • TypeScript Javascript, . , .





  • : permissions. , , Deno, , permissions . , , . ( )





  • , (https://deno.land/x β†’ 460 NPM β†’ + 1 ).





  • , Deno . + , Node, Deno. , , javascript…






"Node.JS Developer".



" Node.js ".













All Articles