Deno Workshop

3rd week

Software Engineer | Studio XID, Inc.

Microsoft MVP

Deno Korea User Group 4th member

TypeScript Korea User Group Organizer

Marktube (Youtube)

Mark Lee

  • API Server Project

  • install mysql

  • nessie : A database migration tool for deno.

  • dex : An SQL query builder port of Knex for Deno

  • oak : A middleware framework for Deno's http server, including a router middleware.

  • denon : denon is the deno replacement for nodemon providing a feature packed and easy to use experience.

  • bcrypt : BCrypt support for hashing and checking passwords

  • djwt : The absolute minimum to make JSON Web Tokens on deno. Based on JWT and JWS specifications.

  • dotenv : Dotenv handling for deno.

➜ docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=1234 --name books-api-mysql mysql:5.7.30         
Unable to find image 'mysql:5.7.30' locally
5.7.30: Pulling from library/mysql
8559a31e96f4: Pull complete 
d51ce1c2e575: Pull complete 
c2344adc4858: Pull complete 
fcf3ceff18fc: Pull complete 
16da0c38dc5b: Pull complete 
b905d1797e97: Pull complete 
4b50d1c6b05c: Pull complete 
d85174a87144: Pull complete 
a4ad33703fa8: Pull complete 
f7a5433ce20d: Pull complete 
3dcd2a278b4a: Pull complete 
Digest: sha256:32f9d9a069f7a735e28fd44ea944d53c61f990ba71460c5c183e610854ca4854
Status: Downloaded newer image for mysql:5.7.30
9808ce150fc5038122f1b6fd40aa33377cd9f0b3c80ab8852e4067a9e79ad7c1

Run MYSQL

➜ docker exec -i -t books-api-mysql bash                                                    
root@9808ce150fc5:/# 

into the Docker

root@9808ce150fc5:/# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.30 MySQL Community Server (GPL)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> 

into the MYSQL

mysql> CREATE DATABASE `books-api` CHARACTER SET utf8 COLLATE utf8_general_ci;
Query OK, 1 row affected (0.01 sec)

mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)

Create Database : books-api

➜ deno run --allow-net --allow-read --allow-write https://deno.land/x/nessie/cli.ts init
Download https://deno.land/x/nessie/cli.ts
Download https://deno.land/x/nessie/cli/state.ts
Download https://deno.land/x/nessie/deps.ts
Download https://deno.land/std@v0.55.0/path/mod.ts
Download https://deno.land/std@v0.55.0/fs/read_json.ts
Download https://deno.land/std@v0.55.0/testing/asserts.ts
Download https://deno.land/x/denomander@0.6.2/mod.ts
Download https://deno.land/x/nessie/clients/AbstractClient.ts
Download https://deno.land/x/nessie/clients/ClientPostgreSQL.ts
Download https://deno.land/x/nessie/cli/utils.ts
Download https://deno.land/x/nessie/types.ts
Download https://deno.land/std@v0.55.0/fmt/colors.ts
Download https://deno.land/std@v0.55.0/testing/diff.ts
Download https://deno.land/std@v0.55.0/path/_constants.ts
Download https://deno.land/std@v0.55.0/path/win32.ts
Download https://deno.land/std@v0.55.0/path/posix.ts
Download https://deno.land/std@v0.55.0/path/common.ts
Download https://deno.land/std@v0.55.0/path/separator.ts
Download https://deno.land/std@v0.55.0/path/_interface.ts
Download https://deno.land/std@v0.55.0/path/glob.ts
Download https://deno.land/x/postgres@v0.4.1/connection_params.ts
Download https://deno.land/x/postgres@v0.4.1/mod.ts
Download https://deno.land/x/postgres@v0.4.1/query.ts
Download https://deno.land/x/denomander@0.6.2/src/Denomander.ts
Download https://deno.land/x/denomander@0.6.2/bootstrap.ts
Download https://deno.land/x/postgres@v0.4.1/connection.ts
Download https://deno.land/x/postgres@v0.4.1/encode.ts
Download https://deno.land/x/postgres@v0.4.1/decode.ts
Download https://deno.land/std@v0.55.0/path/_globrex.ts
Download https://deno.land/x/postgres@v0.4.1/utils.ts
Download https://deno.land/std@v0.55.0/path/_util.ts
Download https://deno.land/x/postgres@v0.4.1/client.ts
Download https://deno.land/x/postgres@v0.4.1/error.ts
Download https://deno.land/x/postgres@v0.4.1/pool.ts
Download https://deno.land/x/denomander@0.6.2/src/Arguments.ts
Download https://deno.land/x/denomander@0.6.2/src/Command.ts
Download https://deno.land/x/denomander@0.6.2/src/Kernel.ts
Download https://deno.land/x/denomander@0.6.2/src/interfaces.ts
Download https://deno.land/x/denomander@0.6.2/src/types.ts
Download https://deno.land/x/denomander@0.6.2/src/Lizard.ts
Download https://deno.land/x/postgres@v0.4.1/oid.ts
Download https://deno.land/x/postgres@v0.4.1/deps.ts
Download https://deno.land/x/postgres@v0.4.1/packet_writer.ts
Download https://deno.land/x/postgres@v0.4.1/packet_reader.ts
Download https://deno.land/x/postgres@v0.4.1/deferred.ts
Download https://deno.land/x/denomander@0.6.2/deps.ts
Download https://deno.land/x/denomander@0.6.2/src/Helper.ts
Download https://deno.land/x/denomander@0.6.2/src/Option.ts
Download https://deno.land/x/denomander@0.6.2/src/Validator.ts
Download https://deno.land/x/denomander@0.6.2/src/Executor.ts
Download https://deno.land/x/denomander@0.6.2/src/Util.ts
Download https://deno.land/std@0.51.0/io/bufio.ts
Download https://deno.land/std@0.51.0/io/util.ts
Download https://deno.land/std@0.51.0/async/deferred.ts
Download https://deno.land/x/checksum@1.2.0/mod.ts
Download https://deno.land/std@v0.51.0/flags/mod.ts
Download https://deno.land/std@v0.51.0/fmt/colors.ts
Download https://deno.land/std@v0.51.0/testing/asserts.ts
Download https://deno.land/x/checksum@1.2.0/hash.ts
Download https://deno.land/std@0.51.0/testing/asserts.ts
Download https://deno.land/std@0.51.0/path/mod.ts
Download https://deno.land/std@0.51.0/encoding/utf8.ts
Download https://deno.land/std@v0.51.0/testing/diff.ts
Download https://deno.land/std@0.51.0/fmt/colors.ts
Download https://deno.land/std@0.51.0/testing/diff.ts
Download https://deno.land/x/checksum@1.2.0/sha1.ts
Download https://deno.land/x/checksum@1.2.0/md5.ts
Download https://deno.land/std@0.51.0/path/win32.ts
Download https://deno.land/std@0.51.0/path/posix.ts
Download https://deno.land/std@0.51.0/path/common.ts
Download https://deno.land/std@0.51.0/path/separator.ts
Download https://deno.land/std@0.51.0/path/interface.ts
Download https://deno.land/std@0.51.0/path/glob.ts
Download https://deno.land/std@0.51.0/path/_constants.ts
Download https://deno.land/std@0.51.0/path/_util.ts
Download https://deno.land/std@0.51.0/path/_globrex.ts
Check https://deno.land/x/nessie/cli.ts

Generates a nessie.config.ts file

import { ClientPostgreSQL } from "https://deno.land/x/nessie/mod.ts";
import { ClientMySQL } from "https://deno.land/x/nessie/mod.ts";
import { ClientSQLite } from "https://deno.land/x/nessie/mod.ts";

/** These are the default config options. */
const clientOptions = {
  migrationFolder: "./db/migrations",
  seedFolder: "./db/seeds",
};

/** Select one of the supported clients */
const clientMySql = new ClientMySQL(clientOptions, {
  hostname: "localhost",
  port: 3306,
  username: "root",
  password: "books-api",
  db: "nessie",
});

/** This is the final config object */
const config = {
  client: clientMySql,
  // Defaults to false, if you want the query builder exposed in migration files, set this to true.
  exposeQueryBuilder: true,
};

export default config;

nessie.config.ts

➜ deno run --allow-net --allow-read --allow-write https://deno.land/x/nessie/cli.ts make create_users
Download https://deno.land/x/nessie/mod.ts
Download https://deno.land/x/nessie/clients/ClientMySQL.ts
Download https://deno.land/x/nessie/clients/ClientSQLite.ts
Download https://deno.land/x/sqlite@v2.0.0/mod.ts
Download https://deno.land/x/mysql@2.2.0/mod.ts
Download https://deno.land/x/sqlite@v2.0.0/src/db.ts
Download https://deno.land/x/sqlite@v2.0.0/src/rows.ts
Download https://deno.land/x/sqlite@v2.0.0/src/constants.ts
Download https://deno.land/x/mysql@2.2.0/src/client.ts
Download https://deno.land/x/mysql@2.2.0/src/connection.ts
Download https://deno.land/x/sqlite@v2.0.0/build/sqlite.js
Download https://deno.land/x/sqlite@v2.0.0/src/wasm.ts
Download https://deno.land/x/sqlite@v2.0.0/src/error.ts
Download https://deno.land/x/mysql@2.2.0/src/constant/errors.ts
Download https://deno.land/x/mysql@2.2.0/src/deferred.ts
Download https://deno.land/x/mysql@2.2.0/src/logger.ts
Download https://deno.land/x/mysql@2.2.0/deps.ts
Download https://deno.land/x/mysql@2.2.0/src/packets/builders/auth.ts
Download https://deno.land/x/mysql@2.2.0/src/packets/builders/query.ts
Download https://deno.land/x/mysql@2.2.0/src/packets/packet.ts
Download https://deno.land/x/mysql@2.2.0/src/packets/parsers/err.ts
Download https://deno.land/x/mysql@2.2.0/src/packets/parsers/handshake.ts
Download https://deno.land/x/mysql@2.2.0/src/packets/parsers/result.ts
Download https://deno.land/x/mysql@2.2.0/src/buffer.ts
Download https://deno.land/x/mysql@2.2.0/src/constant/capabilities.ts
Download https://deno.land/x/mysql@2.2.0/src/constant/mysql_types.ts
Download https://deno.land/x/mysql@2.2.0/src/auth.ts
Download https://deno.land/x/mysql@2.2.0/src/constant/charset.ts
Download https://deno.land/std@0.53.0/async/mod.ts
Download https://deno.land/std@0.53.0/encoding/utf8.ts
Download https://deno.land/x/bytes_formater@1.2.0/mod.ts
Download https://deno.land/x/checksum@1.4.0/mod.ts
Download https://deno.land/x/sha256@v1.0.2/mod.ts
Download https://deno.land/x/sql_builder@1.5.0/util.ts
Download https://deno.land/x/std@0.53.0/log/mod.ts
Download https://deno.land/x/sha256@v1.0.2/deps.ts
Download https://deno.land/std@0.53.0/async/deferred.ts
Download https://deno.land/std@0.53.0/async/delay.ts
Download https://deno.land/std@0.53.0/async/mux_async_iterator.ts
Download https://deno.land/x/checksum@1.4.0/hash.ts
Download https://deno.land/x/bytes_formater@1.2.0/format.ts
Download https://deno.land/x/bytes_formater@1.2.0/deps.ts
Download https://deno.land/x/std@0.53.0/log/logger.ts
Download https://deno.land/x/std@0.53.0/log/handlers.ts
Download https://deno.land/x/std@0.53.0/testing/asserts.ts
Download https://deno.land/x/std@0.53.0/log/levels.ts
Download https://deno.land/x/sqlite@v2.0.0/build/vfs.js
Download https://deno.land/x/std@0.53.0/fmt/colors.ts
Download https://deno.land/x/std@0.53.0/testing/diff.ts
Download https://deno.land/x/std@0.53.0/fs/exists.ts
Download https://deno.land/x/checksum@1.4.0/sha1.ts
Download https://deno.land/x/checksum@1.4.0/md5.ts
Download https://deno.land/std@v0.50.0/fmt/colors.ts
Download https://denopkg.com/chiefbiiko/std-encoding@v1.0.0/mod.ts
Download https://raw.githubusercontent.com/chiefbiiko/std-encoding/v1.0.0/mod.ts
Download https://deno.land/x/base64/base64url.ts
Download https://deno.land/x/base64/base.ts
Check file:///Users/mark/Deno/books-api-deno/nessie.config.ts
Created migration 1593275229293-create_users.ts at /Users/mark/Deno/books-api-deno/db/migrations

Make migration file

import { Migration } from "https://deno.land/x/nessie/mod.ts";
import { Schema } from "https://deno.land/x/nessie/qb.ts";
import { v4 } from "https://deno.land/std/uuid/mod.ts";
import * as bcrypt from "https://deno.land/x/bcrypt/mod.ts";

/** Runs on migrate */
export const up: Migration<Schema> = async ({ queryBuilder }) => {
  queryBuilder.create("Users", (table) => {
    table.string("userId", 36).primary().notNullable();
    table.string("name", 255).notNullable();
    table.string("email", 255).notNullable();
    table.string("password", 255).notNullable();
    table.timestamps();
  });

  const uuid = v4.generate();
  const name = "Mark Lee";
  const email = "2woongjae@gmail.com";
  const hashed = await bcrypt.hash("deno1234");

  queryBuilder.queryString(
    `INSERT INTO Users VALUES ('${uuid}', '${name}', '${email}', '${hashed}', DEFAULT, DEFAULT);`
  );

  return queryBuilder.query;
};

/** Runs on rollback */
export const down: Migration<Schema> = ({ queryBuilder }) => {
  return queryBuilder.drop("Users");
};

Use QueryBuiler

➜ deno run --allow-net --allow-read https://deno.land/x/nessie/cli.ts migrate 
INFO connecting localhost:3306
INFO connected to localhost
Check file:///Users/mark/Deno/books-api-deno/db/migrations/1593275229293-create_users.ts
Migrated 1593275229293-create_users.ts
Migration complete
INFO close connection
 
➜ deno run --allow-net --allow-read https://deno.land/x/nessie/cli.ts rollback
INFO connecting localhost:3306
INFO connected to localhost
Nothing to rollback
INFO close connection

migrate & rollback

import { Application } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";
import router from "./routes.ts";
import notFound from "./not_found.ts";

const port = 4000;

const app = new Application();

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

app.use(notFound);

await app.listen({ port });

main.ts

import { Router } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";
import UserController from "./controllers/UserController.ts";
import BookController from "./controllers/BookController.ts";

const router = new Router();
const userController = new UserController();
const bookController = new BookController();

router
  .post("/login", userController.login)
  .delete("/logout", userController.logout)
  .post("/book", bookController.createBook)
  .get("/book", bookController.getBooks)
  .get("/book/:id", bookController.getBook)
  .patch("/book/:id", bookController.updateBook)
  .delete("/book/:id", bookController.deleteBook);

export default router;

routes.ts

import { Context } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";

export default function notFound(context: Context) {
  context.response.status = 404;
  context.response.body = { error: "Not Found" };
}

not_found.ts

import { RouterContext } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";

export default class UserController {
  public login(context: RouterContext) {
    context.response.body = "login";
  }

  public logout(context: RouterContext) {
    context.response.body = "logout";
  }
}

controllers/UserController.ts

import { RouterContext } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";

export default class BookController {
  public createBook(context: RouterContext) {
    context.response.body = "createBook";
  }

  public getBooks(context: RouterContext) {
    context.response.body = "getBooks";
  }

  public getBook(context: RouterContext) {
    const id = context.params?.id || null;
    context.response.body = "getBook : " + id;
  }

  public updateBook(context: RouterContext) {
    const id = context.params?.id || null;
    context.response.body = "updateBook : " + id;
  }

  public deleteBook(context: RouterContext) {
    const id = context.params?.id || null;
    context.response.body = "deleteBook : " + id;
  }
}

controllers/BookController.ts

PORT=8080

.env

import { config } from "https://raw.githubusercontent.com/pietvanzoen/deno-dotenv/master/mod.ts";
import { Application } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";
import router from "./routes.ts";
import notFound from "./not_found.ts";

const env = config();
const port = Number(env.PORT) || 4000;

const app = new Application();

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

app.use(notFound);

await app.listen({ port });

main.ts

import { RouterContext } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";

export default class UserController {
  public async login(context: RouterContext) {
    const result = await context.request.body();
    console.log(result.type, result.value); // form URLSearchParamsImpl {}

    context.response.body = "login";
  }

  public logout(context: RouterContext) {...}
}

POST /login

interface URLSearchParams {
  append(name: string, value: string): void;
  delete(name: string): void;
  getAll(name: string): string[];
  get(name: string): string | null;
  has(name: string): boolean;
  set(name: string, value: string): void;
  sort(): void;
  forEach(
    callbackfn: (value: string, key: string, parent: this) => void,
    thisArg?: any
  ): void;
  keys(): IterableIterator<string>;
  values(): IterableIterator<string>;
  entries(): IterableIterator<[string, string]>;
  [Symbol.iterator](): IterableIterator<[string, string]>;
  toString(): string;
}

URLSearchParams

import { RouterContext } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";

export default class UserController {
  public async login(context: RouterContext) {
    const { type, value } = (await context.request.body()) as BodyForm;
    console.log(type, value); // form URLSearchParamsImpl {}

    const email = value.get("email");
    const password = value.get("password");

    console.log(email, password); // 2woongjae@gmail.com deno1234

    context.response.body = "login";
  }

  public logout(context: RouterContext) {...}
}

POST /login

import { RouterContext } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";

export default class UserController {
  public async login(context: RouterContext) {
    const { type, value } = (await context.request.body()) as BodyForm;
    console.log(type, value); // form URLSearchParamsImpl {}

    const email = value.get("email");
    const password = value.get("password");
    console.log(email, password); // 2woongjae@gmail.com deno1234

    if (email === null || password === null) {
      context.response.status = 422;
      context.response.body = { error: "email or password required" };
      return;
    }
    
    context.response.body = "login";
  }

  public logout(context: RouterContext) {...}
}

POST /login

import { Client } from "https://deno.land/x/mysql/mod.ts";
import { connectionOptions } from "../nessie.config.ts";

const client = await new Client().connect(connectionOptions);

export default client;

models/config.ts

import { ClientMySQL } from "https://deno.land/x/nessie/mod.ts";

/** These are the default config options. */
const clientOptions = {
  migrationFolder: "./db/migrations",
  seedFolder: "./db/seeds",
};

export const connectionOptions = {
  hostname: "localhost",
  port: 3306,
  username: "root",
  password: "1234",
  db: "books-api",
};

/** Select one of the supported clients */
const clientMySql = new ClientMySQL(clientOptions, connectionOptions);

/** This is the final config object */
const config = {
  client: clientMySql,
  // Defaults to false, if you want the query builder exposed in migration files, set this to true.
  exposeQueryBuilder: true,
};

export default config;

nessie.config.ts

import Dex from "https://deno.land/x/dex/mod.ts";
import client from "./config.ts";

const dex = Dex({ client: "mysql" });

export interface User {
  userId: string;
  name: string;
  email: string;
  password: string;
  created_at: Date;
  updated_at: Date;
}

export async function getUserByEmail(email: string): Promise<User> {
  const selectQuery = dex
    .queryBuilder()
    .select()
    .from("Users")
    .where({ email })
    .toString();
  const { rows } = await client.execute(selectQuery);
  if (rows === undefined) throw new Error();
  if (rows.length !== 1) throw new Error();
  return rows[0];
}

models/Users.ts

import { RouterContext } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";

export default class UserController {
  public async login(context: RouterContext) {
    const { type, value } = (await context.request.body()) as BodyForm;
    console.log(type, value);

    const email = value.get("email");
    const password = value.get("password");
    console.log(email, password);

    if (email === null || password === null) {
      context.response.status = 422;
      context.response.body = { error: "email or password required" };
      return;
    }

    const user = await getUserByEmail(email);
    console.log(user);

    const match = await bcrypt.compare(password, user.password);
    console.log(match);

    context.response.body = "login";
  }

  public logout(context: RouterContext) {...}
}

POST /login

import {
  makeJwt,
  setExpiration,
  Jose,
} from "https://deno.land/x/djwt/create.ts";
import { User } from "../models/Users.ts";

const key = "markzzang";

const header: Jose = {
  alg: "HS256",
  typ: "JWT",
};

export function generate(user: User): string {
  return makeJwt({
    header,
    payload: {
      iss: "mark",
      exp: setExpiration(new Date().getTime() + 60000),
      userId: user.userId,
      email: user.email,
    },
    key,
  });
}

models/config.ts

import {
  RouterContext,
  BodyForm,
} from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";
import * as bcrypt from "https://deno.land/x/bcrypt/mod.ts";
import { getUserByEmail } from "../models/Users.ts";
import { generate } from "../utils/token.ts";

export default class UserController {
  public async login(context: RouterContext) {
    ...

    const user = await getUserByEmail(email);
    console.log(user);

    const match = await bcrypt.compare(password, user.password);
    console.log(match);

    if (match) {
      context.response.body = { token: generate(user) };
    } else {
      context.response.status = 400;
      context.response.body = { error: "wrong password" };
    }
  }

  public logout(context: RouterContext) {...}
}

models/config.ts

➜ deno run --allow-net --allow-read --allow-write https://deno.land/x/nessie/cli.ts make create_books 


Check file:///Users/mark/Deno/books-api-deno/nessie.config.ts
Created migration 1593422520239-create_books.ts at /Users/mark/Deno/books-api-deno/db/migrations

Create Books Table

import { Migration } from "https://deno.land/x/nessie/mod.ts";
import { Schema } from "https://deno.land/x/nessie/qb.ts";

/** Runs on migrate */
export const up: Migration<Schema> = async ({ queryBuilder }) => {
  queryBuilder.create("Books", (table) => {
    table.id();
    table.string("ownerId", 36).notNullable();
    table.string("title", 255).notNullable();
    table.string("message", 255).notNullable();
    table.string("author", 255).notNullable();
    table.string("url", 255).notNullable();
    table.timestamps();
  });

  return queryBuilder.query;
};

/** Runs on rollback */
export const down: Migration<Schema> = ({ queryBuilder }) => {
  return queryBuilder.drop("Books");
};

Create Books Table

➜ deno run --allow-net --allow-read --unstable https://deno.land/x/nessie/cli.ts migrate           
INFO connecting localhost:3306
INFO connected to localhost
Check file:///Users/mark/Deno/books-api-deno/db/migrations/1593422520239-create_books.ts
Migrated 1593422520239-create_books.ts
Migration complete
INFO close connection

Mirgate Books Table

import { RouterContext } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";
import { validate } from "../utils/token.ts";
import { getBooksByOwnerId } from "../models/Books.ts";

export default class BookController {
  public async getBooks(context: RouterContext) {
    console.log(context.request.headers);

    const authorization = context.request.headers.get("authorization");
    console.log(authorization);
  }
}

GET /book

import { RouterContext } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";
import { validate } from "../utils/token.ts";
import { getBooksByOwnerId } from "../models/Books.ts";

export default class BookController {
  public async getBooks(context: RouterContext) {
    console.log(context.request.headers);

    const authorization = context.request.headers.get("authorization");
    console.log(authorization);

    if (authorization === null) {
      context.response.status = 401;
      context.response.body = { error: "Unauthorized" };
      return;
    }

    const token = authorization.replace("Bearer ", "");
    console.log(token);

    const validation = await validate(token);
    console.log(validation);
  }
}

GET /book

import {
  makeJwt,
  setExpiration,
  Jose,
} from "https://deno.land/x/djwt/create.ts";
import {
  validateJwt,
  JwtValidation,
} from "https://deno.land/x/djwt/validate.ts";
import { User } from "../models/Users.ts";

const key = "markzzang";

const header: Jose = {
  alg: "HS256",
  typ: "JWT",
};

export function generate(user: User): string {...}

export async function validate(token: string): Promise<JwtValidation> {
  return await validateJwt(token, key);
}

utils/token.ts

import { RouterContext } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";
import { validate } from "../utils/token.ts";
import { getBooksByOwnerId } from "../models/Books.ts";

export default class BookController {
  public async getBooks(context: RouterContext) {
    ...

    const validation = await validate(token);
    console.log(validation);

    if (!validation.isValid) {
      context.response.status = 401;
      context.response.body = { error: "Unauthorized" };
      return;
    }

    const userId = validation.payload?.userId as string;

    if (userId === undefined) {
      context.response.status = 401;
      context.response.body = { error: "Unauthorized" };
      return;
    }

    const books = await getBooksByOwnerId(userId);

    context.response.body = books;
  }
}

GET /book

import { config } from "https://raw.githubusercontent.com/pietvanzoen/deno-dotenv/master/mod.ts";
import { Application } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";
import router from "./routes.ts";
import notFound from "./not_found.ts";

const env = config();
const port = Number(env.PORT) || 4000;

const app = new Application();

app.use(async (context, next) => {
  try {
    await next();
  } catch (error) {
    context.response.status = error.status || 400;
    context.response.body = { error: error.message };
  }
});

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

app.use(notFound);

await app.listen({ port });

Error Handling

export class UnauthorizedError extends Error {
  status = 401;
  message = "Unauthorized Error";
}

utils/errors.ts

import { RouterContext } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";
import { validate } from "../utils/token.ts";
import { getBooksByOwnerId } from "../models/Books.ts";
import { UnauthorizedError } from "../utils/errors.ts";

export default class BookController {
  public async getBooks(context: RouterContext) {
    console.log(context.request.headers);

    const authorization = context.request.headers.get("authorization");
    console.log(authorization);

    if (authorization === null) {
      throw new UnauthorizedError();
    }

    const token = authorization.replace("Bearer ", "");
    console.log(token);

    const validation = await validate(token);
    console.log(validation);

    if (!validation.isValid) {
      throw new UnauthorizedError();
    }

    const userId = validation.payload?.userId as string;

    if (userId === undefined) {
      throw new UnauthorizedError();
    }

    const books = await getBooksByOwnerId(userId);

    context.response.body = books;
  }
}

GET /book

import {
  RouterContext,
  BodyForm,
} from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";
import { validate } from "../utils/token.ts";
import { getBooksByOwnerId, createBook } from "../models/Books.ts";
import { UnauthorizedError, ParameterRequired } from "../utils/errors.ts";

export default class BookController {
  public async createBook(context: RouterContext) {
    const authorization = context.request.headers.get("authorization");

    if (authorization === null) {
      throw new UnauthorizedError();
    }

    const token = authorization.replace("Bearer ", "");
    const validation = await validate(token);

    if (!validation.isValid) {
      throw new UnauthorizedError();
    }

    const userId = validation.payload?.userId as string;

    if (userId === undefined) {
      throw new UnauthorizedError();
    }

    const { value } = (await context.request.body()) as BodyForm;

    const title = value.get("title");
    const message = value.get("message");
    const author = value.get("author");
    const url = value.get("url");

    if (title === null || message === null || author === null || url === null) {
      throw new ParameterRequired();
    }

    await createBook({ title, message, author, url }, userId);

    context.response.body = { success: true };
  }
}

POST /book

export interface BookRawData {
  title: string;
  message: string;
  author: string;
  url: string;
}

export async function createBook(
  book: BookRawData,
  ownerId: string
): Promise<void> {
  const insertQuery = dex
    .queryBuilder()
    .insert([{ ...book, ownerId }])
    .into("Books")
    .toString();
  console.log(insertQuery);
  await client.execute(insertQuery);
}

models/Books.ts

import {
  RouterContext,
  BodyForm,
} from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";
import { validate } from "../utils/token.ts";
import { getBooksByOwnerId, createBook, getBookById } from "../models/Books.ts";
import { UnauthorizedError, ParameterRequired } from "../utils/errors.ts";

export default class BookController {
  public async getBook(context: RouterContext) {
    const authorization = context.request.headers.get("authorization");

    if (authorization === null) {
      throw new UnauthorizedError();
    }

    const token = authorization.replace("Bearer ", "");
    const validation = await validate(token);

    if (!validation.isValid) {
      throw new UnauthorizedError();
    }

    const userId = validation.payload?.userId as string;

    if (userId === undefined) {
      throw new UnauthorizedError();
    }

    const bookId = Number(context.params?.id) || null;

    if (bookId === null) {
      throw new ParameterRequired();
    }

    const book = await getBookById(bookId, userId);

    context.response.body = book;
  }
}

GET /book/:id

export async function getBookById(id: number, ownerId: string): Promise<Book> {
  const selectQuery = dex
    .queryBuilder()
    .select()
    .from("Books")
    .where({ id, ownerId })
    .toString();
  const { rows } = await client.execute(selectQuery);
  if (rows === undefined) throw new Error();
  if (rows.length !== 1) throw new Error("result not exist");
  return rows[0];
}

GET /book/:id

import { RouterContext } from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";
import { UnauthorizedError } from "./errors.ts";
import { validate } from "./token.ts";

export async function getUserIdFromAuthorization(
  context: RouterContext
): Promise<string> {
  const authorization = context.request.headers.get("authorization");

  if (authorization === null) {
    throw new UnauthorizedError();
  }

  const token = authorization.replace("Bearer ", "");
  const validation = await validate(token);

  if (!validation.isValid) {
    throw new UnauthorizedError();
  }

  const userId = validation.payload?.userId as string;

  if (userId === undefined) {
    throw new UnauthorizedError();
  }

  return userId;
}

utils/auth.ts

import {
  RouterContext,
  BodyForm,
} from "https://raw.githubusercontent.com/oakserver/oak/main/mod.ts";
import { validate } from "../utils/token.ts";
import { getBooksByOwnerId, createBook, getBookById } from "../models/Books.ts";
import { UnauthorizedError, ParameterRequired } from "../utils/errors.ts";
import { getUserIdFromAuthorization } from "../utils/auth.ts";

export default class BookController {
  public async getBook(context: RouterContext) {
    const userId = await getUserIdFromAuthorization(context);

    const bookId = Number(context.params?.id) || null;

    if (bookId === null) {
      throw new ParameterRequired();
    }

    const book = await getBookById(bookId, userId);

    context.response.body = book;
  }
}

utils/auth.ts