Recentemente escrevi sobre como fazer uma API Todo em Deno + Oak (sem usar um banco de dados) . Você pode encontrar o repositório em chapter_1:oak no GitHub.
Este tutorial continua de onde o outro parou, e vou falar sobre como integrar o MySQL em um projeto Deno e Oak.
Se a qualquer momento você quiser ver todo o código-fonte usado neste tutorial, ele está disponível em chapter_2:mysql . Sinta-se à vontade para dar uma estrela no GitHub se gostar.
Estou assumindo que você já completou o último tutorial mencionado acima. Se não, confira aqui e volte quando terminar.
Antes de começarmos, verifique se você tem um cliente MySQL instalado e em execução:
- Servidor da comunidade MySQL [Baixe aqui]
- MySQL Workbench [Baixe aqui]
Eu escrevi um pequeno guia para usuários do Mac OS sobre como configurar o MySQL porque também tive dificuldades com isso. Confira aqui.
Se você estiver em uma máquina Windows, você pode usar as mesmas ferramentas ou também pode usar o XAMPP para ter uma instância MySQL em execução em seu painel.
Assim que você tiver uma instância MySQL em execução, podemos começar nosso tutorial.
Vamos começar
Supondo que você esteja vindo deste artigo, Todo API em Deno + Oak (sem usar um banco de dados) , faremos o seguinte:
- Criar uma conexão de banco de dados MySQL
- Escreva um pequeno script que redefina o banco de dados toda vez que iniciamos nosso servidor Deno
- Executar operações CRUD em uma tabela
- Adicione a funcionalidade CRUD aos nossos controladores de API
Uma última coisa – aqui está toda a diferença de commit que foi feita no Capítulo 1 para adicionar o MySQL ao projeto (código-fonte que mostra as novas adições feitas no capítulo 1).
Na pasta raiz do seu projeto - o meu é chamado
chapter_2:mysql
, embora o seu pode ser chamado como você quiser - crie uma pasta chamada db . Dentro dessa pasta, crie um arquivo chamado config.ts e adicione o seguinte conteúdo a ele:export const DATABASE: string = "deno";
export const TABLE = {
TODO: "todo",
};
Nada extravagante aqui, apenas definindo nosso nome de banco de dados junto com um objeto para tabelas e depois exportando-o. Nosso projeto terá um banco de dados chamado "deno" e dentro desse banco de dados teremos apenas uma tabela chamada "todo".
Em seguida, dentro do db pasta, crie outro arquivo chamado client.ts e adicione o seguinte conteúdo:
import { Client } from "https://deno.land/x/mysql/mod.ts";
// config
import { DATABASE, TABLE } from "./config.ts";
const client = await new Client();
client.connect({
hostname: "127.0.0.1",
username: "root",
password: "",
db: "",
});
export default client;
Algumas coisas estão acontecendo aqui.
Estamos importando
Client
do mysql
biblioteca. Client
nos ajudará a conectar-se ao nosso banco de dados e realizar operações no banco de dados. client.connect({
hostname: "127.0.0.1",
username: "root",
password: "",
db: "",
});
Client
fornece um método chamado connect
que recebe um objeto onde podemos fornecer o hostname
, username
, password
e db
. Com essas informações ele pode estabelecer uma conexão com nossa instância MySQL. Certifique-se de que seu
username
não tem password
, pois entrará em conflito com a conexão com a biblioteca MySQL do Deno. Se você não sabe como fazer isso, leia este tutorial que escrevi. Saí do
database
campo em branco aqui porque quero selecioná-lo manualmente mais tarde no meu script. Vamos adicionar um script que irá inicializar um banco de dados chamado "deno", selecioná-lo e dentro desse banco de dados criar uma tabela chamada "todo".
Dentro de
db/client.ts
arquivo vamos fazer algumas novas adições:import { Client } from "https://deno.land/x/mysql/mod.ts";
// config
import { DATABASE, TABLE } from "./config.ts";
const client = await new Client();
client.connect({
hostname: "127.0.0.1",
username: "root",
password: "",
db: "",
});
const run = async () => {
// create database (if not created before)
await client.execute(`CREATE DATABASE IF NOT EXISTS ${DATABASE}`);
// select db
await client.execute(`USE ${DATABASE}`);
// delete table if it exists before
await client.execute(`DROP TABLE IF EXISTS ${TABLE.TODO}`);
// create table
await client.execute(`
CREATE TABLE ${TABLE.TODO} (
id int(11) NOT NULL AUTO_INCREMENT,
todo varchar(100) NOT NULL,
isCompleted boolean NOT NULL default false,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`);
};
run();
export default client;
Aqui estamos importando
DATABASE
e TABLE
do nosso arquivo de configuração, usando esses valores em uma nova função chamada run()
. Vamos detalhar este
run()
função. Adicionei comentários no arquivo para ajudar você a entender o fluxo de trabalho:const run = async () => {
// create database (if not created before)
await client.execute(`CREATE DATABASE IF NOT EXISTS ${DATABASE}`);
// select db
await client.execute(`USE ${DATABASE}`);
// delete table if it exists before
await client.execute(`DROP TABLE IF EXISTS ${TABLE.TODO}`);
// create table
await client.execute(`
CREATE TABLE ${TABLE.TODO} (
id int(11) NOT NULL AUTO_INCREMENT,
todo varchar(100) NOT NULL,
isCompleted boolean NOT NULL default false,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`);
};
run();
- Crie um banco de dados chamado
deno
. Se já existir, não faça nada. - Em seguida, selecione o banco de dados a ser usado, chamado
deno
- Excluir a tabela dentro de
deno
chamadotodo
se já existe. - Em seguida, crie uma nova tabela dentro do
deno
db, chame-o detodo
, e defina sua estrutura:Ele terá um incremento automático exclusivoid
que será um inteiro, outro campo chamadotodo
que será uma string e, finalmente, um campo chamadoisCompleted
que é um booleano. Eu também definoid
como minha chave primária.
A razão pela qual escrevi este script foi porque não quero ter informações extras na instância do MySQL. Toda vez que o script é executado, ele reinicializa tudo.
Você não precisa adicionar este script. Mas se você não fizer isso, você terá que criar manualmente um banco de dados e a tabela.
Além disso, confira os documentos da biblioteca Deno MySQL sobre criação de banco de dados e criação de tabela.
Voltando à nossa agenda, acabamos de alcançar duas coisas das quatro mencionadas no início do artigo:
- Criar uma conexão de banco de dados MySQL
- Escreva um pequeno script que redefina o banco de dados toda vez que iniciamos nosso servidor Deno
Isso já é 50% do tutorial. Infelizmente, não podemos ver muita coisa acontecendo agora. Vamos adicionar rapidamente algumas funcionalidades para que funcionem.
Executar operações CRUD em uma tabela e adicionar a funcionalidade aos nossos controladores de API
Precisamos atualizar nossa interface Todo primeiro. Vá para as
interfaces/Todo.ts
arquivo e adicione o seguinte:export default interface Todo {
id?: number,
todo?: string,
isCompleted?: boolean,
}
O que isso
?
faz é que torna a chave no objeto opcional. Fiz isso porque mais tarde vou usar funções diferentes para passar objetos com apenas um id
, todo
, isCompleted
, ou todos eles de uma vez. Se você quiser saber mais sobre propriedades opcionais no TypeScript, acesse seus documentos aqui.
Em seguida, crie uma nova pasta chamada modelos e dentro dessa pasta, crie um arquivo chamado todo.ts . Adicione o seguinte conteúdo ao arquivo:
import client from "../db/client.ts";
// config
import { TABLE } from "../db/config.ts";
// Interface
import Todo from "../interfaces/Todo.ts";
export default {
/**
* Takes in the id params & checks if the todo item exists
* in the database
* @param id
* @returns boolean to tell if an entry of todo exits in table
*/
doesExistById: async ({ id }: Todo) => {},
/**
* Will return all the entries in the todo column
* @returns array of todos
*/
getAll: async () => {},
/**
* Takes in the id params & returns the todo item found
* against it.
* @param id
* @returns object of todo item
*/
getById: async ({ id }: Todo) => {},
/**
* Adds a new todo item to todo table
* @param todo
* @param isCompleted
*/
add: async (
{ todo, isCompleted }: Todo,
) => {},
/**
* Updates the content of a single todo item
* @param id
* @param todo
* @param isCompleted
* @returns integer (count of effect rows)
*/
updateById: async ({ id, todo, isCompleted }: Todo) => {},
/**
* Deletes a todo by ID
* @param id
* @returns integer (count of effect rows)
*/
deleteById: async ({ id }: Todo) => {},
};
Agora as funções estão vazias, mas tudo bem. Vamos preenchê-los um por um.
Em seguida, vá para
controllers/todo.ts
arquivo e certifique-se de adicionar o seguinte:// interfaces
import Todo from "../interfaces/Todo.ts";
// models
import TodoModel from "../models/todo.ts";
export default {
/**
* @description Get all todos
* @route GET /todos
*/
getAllTodos: async ({ response }: { response: any }) => {},
/**
* @description Add a new todo
* @route POST /todos
*/
createTodo: async (
{ request, response }: { request: any; response: any },
) => {},
/**
* @description Get todo by id
* @route GET todos/:id
*/
getTodoById: async (
{ params, response }: { params: { id: string }; response: any },
) => {},
/**
* @description Update todo by id
* @route PUT todos/:id
*/
updateTodoById: async (
{ params, request, response }: {
params: { id: string };
request: any;
response: any;
},
) => {},
/**
* @description Delete todo by id
* @route DELETE todos/:id
*/
deleteTodoById: async (
{ params, response }: { params: { id: string }; response: any },
) => {},
};
Aqui também temos funções vazias. Vamos começar a preenchê-los.
[Obter] todas as APIs de todos
Dentro de
models/todo.ts
, adicione uma definição para uma função chamada getAll
:import client from "../db/client.ts";
// config
import { TABLE } from "../db/config.ts";
// Interface
import Todo from "../interfaces/Todo.ts";
export default {
/**
* Will return all the entries in the todo column
* @returns array of todos
*/
getAll: async () => {
return await client.query(`SELECT * FROM ${TABLE.TODO}`);
},
}
O
Client
também expõe outro método além de connect
(usamos um método "connect" em db/client.ts
file) e isso é query
. O client.query
O método nos permite executar consultas MySQL diretamente do nosso código Deno como está. Em seguida, vá para
controllers/todo.ts
adicionar definição para getAllTodos
:// interfaces
import Todo from "../interfaces/Todo.ts";
// models
import TodoModel from "../models/todo.ts";
export default {
/**
* @description Get all todos
* @route GET /todos
*/
getAllTodos: async ({ response }: { response: any }) => {
try {
const data = await TodoModel.getAll();
response.status = 200;
response.body = {
success: true,
data,
};
} catch (error) {
response.status = 400;
response.body = {
success: false,
message: `Error: ${error}`,
};
}
},
}
Tudo o que estamos fazendo é importar
TodoModel
e usando seu método chamado getAll
, que acabamos de definir agora. Como ele retorna como uma promessa, nós o envolvemos em async/await. O método
TodoModel.getAll()
nos retornará um array que simplesmente retornaremos para response.body
com status
definido como 200
. Se a promessa falhar ou houver outro erro, simplesmente vamos ao nosso bloco catch e retornamos o status 400 com
success
definido como falso. Também definimos a message
ao que obtemos do bloco catch. É isso, terminamos. Agora vamos abrir nosso terminal.
Verifique se sua instância do MySQL está em execução. No seu tipo de terminal:
$ deno run --allow-net server.ts
Seu terminal deve ficar assim:
Meu console está me dizendo duas coisas aqui.
- Que meu servidor Deno API está sendo executado na porta 8080
- Que minha instância do MySQL está sendo executada em
127.0.0.1
, que élocalhost
Vamos testar nossa API. Estou usando o Postman aqui, mas você pode usar seu cliente de API favorito.
No momento, ele retorna apenas dados vazios. Mas uma vez que adicionamos dados ao nosso
todo
table, ele retornará esses todos aqui. Impressionante. Uma API inativa e mais quatro para ir.
[Post] adicionar uma API de tarefas
No
models/todo.ts
arquivo, adicione a seguinte definição para add()
função:export default {
/**
* Adds a new todo item to todo table
* @param todo
* @param isCompleted
*/
add: async (
{ todo, isCompleted }: Todo,
) => {
return await client.query(
`INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)`,
[
todo,
isCompleted,
],
);
},
}
A função add recebe objeto como argumento, que tem dois itens:
todo
e isCompleted
. Então
add: async ({ todo, isCompleted }: Todo) => {}
também pode ser escrito como ({todo, isCompleted}: {todo:string, isCompleted:boolean})
. Mas como já temos uma interface definida em nosso interfaces/Todo.ts
arquivo que é export default interface Todo {
id?: number,
todo?: string,
isCompleted?: boolean,
}
podemos simplesmente escrever isso como
add: async ({ todo, isCompleted }: Todo) => {}
. Isso diz ao TypeScript que esta função tem dois argumentos, todo
, que é uma string, e isCompleted
, que é um booleano. Se você quiser ler mais sobre interfaces, o TypeScript tem um excelente documento que você pode encontrar aqui.
Dentro da nossa função temos o seguinte:
return await client.query(
`INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)`,
[
todo,
isCompleted,
],
);
Esta consulta pode ser dividida em duas partes:
INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)
. Os dois pontos de interrogação aqui denotam o uso de variáveis dentro desta consulta.- A outra parte,
[todo, isCompleted]
, são as variáveis que irão na primeira parte da consulta e ser substituído por(?, ?)
Table.Todo
é apenas uma string vinda do arquivodb/config.ts
onde oTable.Todo
valor é "todo
"
Em seguida, dentro de nosso
controllers/todo.ts
arquivo, vá para a definição do createTodo()
função:export default {
/**
* @description Add a new todo
* @route POST /todos
*/
createTodo: async (
{ request, response }: { request: any; response: any },
) => {
const body = await request.body();
if (!request.hasBody) {
response.status = 400;
response.body = {
success: false,
message: "No data provided",
};
return;
}
try {
await TodoModel.add(
{ todo: body.value.todo, isCompleted: false },
);
response.body = {
success: true,
message: "The record was added successfully",
};
} catch (error) {
response.status = 400;
response.body = {
success: false,
message: `Error: ${error}`,
};
}
},
}
Vamos dividir isso em duas partes:
Parte 1
const body = await request.body();
if (!request.hasBody) {
response.status = 400;
response.body = {
success: false,
message: "No data provided",
};
return;
}
Tudo o que estamos fazendo aqui é verificar se o usuário está enviando dados no corpo. Caso contrário, retornaremos um status
400
e no corpo retorne success: false
e message: <erromessage-string>
. Parte 2
try {
await TodoModel.add(
{ todo: body.value.todo, isCompleted: false },
);
response.body = {
success: true,
message: "The record was added successfully",
};
} catch (error) {
response.status = 400;
response.body = {
success: false,
message: `Error: ${error}`,
};
}
Se não houver erro, o
TodoModel.add()
função é chamada e simplesmente retorna um status de 200
e uma mensagem de confirmação para o usuário. Caso contrário, apenas lança um erro semelhante ao que fizemos na API anterior.
Agora terminamos. Ative seu terminal e verifique se sua instância MySQL está em execução. No seu tipo de terminal:
$ deno run --allow-net server.ts
Vá para o Postman e execute a rota da API para este controlador:
Isso é ótimo, agora temos duas APIs funcionando. Faltam apenas três.
[GET] todo por id API
Em seu
models/todo.ts
arquivo, adicione definição para essas duas funções, doesExistById()
e getById()
:export default {
/**
* Takes in the id params & checks if the todo item exists
* in the database
* @param id
* @returns boolean to tell if an entry of todo exits in table
*/
doesExistById: async ({ id }: Todo) => {
const [result] = await client.query(
`SELECT COUNT(*) count FROM ${TABLE.TODO} WHERE id = ? LIMIT 1`,
[id],
);
return result.count > 0;
},
/**
* Takes in the id params & returns the todo item found
* against it.
* @param id
* @returns object of todo item
*/
getById: async ({ id }: Todo) => {
return await client.query(
`SELECT * FROM ${TABLE.TODO} WHERE id = ?`,
[id],
);
},
}
Vamos falar sobre cada função uma por uma:
doesExistById
recebe umid
e retorna umboolean
indicando se uma tarefa específica existe ou não no banco de dados.
Vamos decompor esta função:
const [result] = await client.query(
`SELECT COUNT(*) count FROM ${TABLE.TODO} WHERE id = ? LIMIT 1`,
[id],
);
return result.count > 0;
Simplesmente verificamos a contagem aqui na tabela em relação a um ID de tarefas específico. Se a contagem for maior que zero, retornamos
true
. Caso contrário, retornamos false
. getById
retorna o item todo em relação a um id específico:
return await client.query(
`SELECT * FROM ${TABLE.TODO} WHERE id = ?`,
[id],
);
Estamos simplesmente executando uma consulta MySQL aqui para obter um todo por id e retornar o resultado como está.
Em seguida, vá para seu
controllers/todo.ts
e adicione uma definição para um getTodoById
método do controlador:export default {
/**
* @description Get todo by id
* @route GET todos/:id
*/
getTodoById: async (
{ params, response }: { params: { id: string }; response: any },
) => {
try {
const isAvailable = await TodoModel.doesExistById(
{ id: Number(params.id) },
);
if (!isAvailable) {
response.status = 404;
response.body = {
success: false,
message: "No todo found",
};
return;
}
const todo = await TodoModel.getById({ id: Number(params.id) });
response.status = 200;
response.body = {
success: true,
data: todo,
};
} catch (error) {
response.status = 400;
response.body = {
success: false,
message: `Error: ${error}`,
};
}
},
}
Vamos dividir isso em duas partes menores:
const isAvailable = await TodoModel.doesExistById(
{ id: Number(params.id) },
);
if (!isAvailable) {
response.status = 404;
response.body = {
success: false,
message: "No todo found",
};
return;
}
Primeiro, verificamos se o todo existe no banco de dados em relação a um id usando este método:
const isAvailable = await TodoModel.doesExistById(
{ id: Number(params.id) },
);
Aqui precisamos converter
params.id
em um Number
porque nossa interface de tarefas só aceita id
como um número. Em seguida, apenas passamos params.id
para o doesExistById
método. Este método retornará como um booleano. Então, simplesmente verificamos se o todo não está disponível e retornamos um
404
método com nossa resposta padrão, como nos endpoints anteriores:if (!isAvailable) {
response.status = 404;
response.body = {
success: false,
message: "No todo found",
};
return;
}
Então nós temos:
try {
const todo: Todo = await TodoModel.getById({ id: Number(params.id) });
response.status = 200;
response.body = {
success: true,
data: todo,
};
} catch (error) {
response.status = 400;
response.body = {
success: false,
message: `Error: ${error}`,
};
Isso é semelhante ao que estávamos fazendo em nossas APIs anteriores. Aqui estamos simplesmente obtendo dados do banco de dados, definindo a variável
todo
e, em seguida, retornando a resposta. Se houver um erro, simplesmente retornamos uma mensagem de erro padrão no bloco catch para o usuário. Agora inicie seu terminal e verifique se sua instância MySQL está em execução. No seu tipo de terminal:
$ deno run --allow-net server.ts
Vá para o Postman e execute a rota da API para este controlador.
Lembre-se que toda vez que reiniciamos nosso servidor nós resetamos o db. Se você não quiser esse comportamento, você pode simplesmente comentar o
run
função no arquivo db/client.ts
. retornará o todo para esse id se encontrado" width="2000" height="1165" loading=" preguiçoso">
Até agora, fizemos APIs para:
- Obter todos
- Criar uma nova tarefa
- Obter uma tarefa por ID
E aqui estão as APIs restantes:
- Atualizar uma tarefa por ID
- Excluir uma tarefa por ID
[PUT] atualizar todo pela API de id
Vamos criar um modelo para esta API primeiro. Acesse nosso
models/todo.ts
arquivo e adicione uma definição para um updateById
função:**
* Updates the content of a single todo item
* @param id
* @param todo
* @param isCompleted
* @returns integer (count of effect rows)
*/
updateById: async ({ id, todo, isCompleted }: Todo) => {
const result = await client.query(
`UPDATE ${TABLE.TODO} SET todo=?, isCompleted=? WHERE id=?`,
[
todo,
isCompleted,
id,
],
);
// return count of rows updated
return result.affectedRows;
},
O
updateById
leva em 3 parâmetros:id
, todo
e isCompleted
. Simplesmente executamos uma consulta MySQL dentro desta função:
onst result = await client.query(
`UPDATE ${TABLE.TODO} SET todo=?, isCompleted=? WHERE id=?`,
[
todo,
isCompleted,
id,
],
);
Isso atualiza o
todo
de um único item de tarefas e isCompleted
por um id
específico . Em seguida, retornamos uma contagem de linhas atualizadas por esta consulta fazendo:
// return count of rows updated
return result.affectedRows;
A contagem será 0 ou 1, mas nunca mais que 1. Isso ocorre porque temos IDs exclusivos em nosso banco de dados – vários todos com o mesmo ID não podem existir.
Em seguida, vá para nossos
controllers/todo.ts
arquivo e adicione uma definição para um updateTodoById
função:updateTodoById: async (
{ params, request, response }: {
params: { id: string };
request: any;
response: any;
},
) => {
try {
const isAvailable = await TodoModel.doesExistById(
{ id: Number(params.id) },
);
if (!isAvailable) {
response.status = 404;
response.body = {
success: false,
message: "No todo found",
};
return;
}
// if todo found then update todo
const body = await request.body();
const updatedRows = await TodoModel.updateById({
id: Number(params.id),
...body.value,
});
response.status = 200;
response.body = {
success: true,
message: `Successfully updated ${updatedRows} row(s)`,
};
} catch (error) {
response.status = 400;
response.body = {
success: false,
message: `Error: ${error}`,
};
}
},
Isso é quase o mesmo de nossas APIs anteriores que escrevemos. A parte que é nova aqui é esta:
// if todo found then update todo
const body = await request.body();
const updatedRows = await TodoModel.updateById({
id: Number(params.id),
...body.value,
});
Nós simplesmente pegamos o corpo que o usuário nos envia em JSON e passamos o corpo para nosso
TodoModel.updateById
função. Temos que converter o
id
para um número para cumprir com nossa interface Todo. A consulta é executada e retorna a contagem de linhas atualizadas. A partir daí, simplesmente o devolvemos em nossa resposta. Se houver um erro, ele vai para o bloco catch onde retornamos nossa mensagem de resposta padrão.
Vamos executar isso e ver se funciona. Verifique se sua instância do MySQL está em execução e execute o seguinte no seu terminal:
$ deno run --allow-net server.ts
Vá para o Postman e execute a rota da API para este controlador:
[DELETE] todo por id API
Em seu
models/todo.ts
arquivo crie uma função chamada deleteById
:/**
* Deletes a todo by ID
* @param id
* @returns integer (count of effect rows)
*/
deleteById: async ({ id }: Todo) => {
const result = await client.query(
`DELETE FROM ${TABLE.TODO} WHERE id = ?`,
[id],
);
// return count of rows updated
return result.affectedRows;
},
Aqui nós simplesmente passamos um
id
como um parâmetro e, em seguida, use a consulta de exclusão do MySQL. Em seguida, retornamos a contagem atualizada de linhas. A contagem atualizada será 0 ou 1 porque o ID de cada tarefa é exclusivo. Em seguida, vá em seu
controllers/todo.ts
arquivo e defina um deleteByTodoId
método:/**
* @description Delete todo by id
* @route DELETE todos/:id
*/
deleteTodoById: async (
{ params, response }: { params: { id: string }; response: any },
) => {
try {
const updatedRows = await TodoModel.deleteById({
id: Number(params.id),
});
response.status = 200;
response.body = {
success: true,
message: `Successfully updated ${updatedRows} row(s)`,
};
} catch (error) {
response.status = 400;
response.body = {
success: false,
message: `Error: ${error}`,
};
}
},
Isso é bem direto. Passamos o
params.id
ao nosso TodoModel.deleteById
e retornar a contagem de linhas atualizadas com esta consulta. Se algo der errado, um erro é lançado no bloco catch que retorna nossa resposta de erro padrão.
Vamos verificar isso.
Verifique se sua instância do MySQL está em execução. No seu tipo de terminal:
$ deno run --allow-net server.ts
Vá para o Postman e execute a rota da API para este controlador:
Com isso, terminamos com nosso tutorial Deno + Oak + MySQL.
Todo o código fonte está disponível aqui:https://github.com/adeelibr/deno-playground. Se você encontrar um problema, é só me avisar. Ou sinta-se à vontade para fazer um pull request e eu lhe darei crédito no repositório.
Se você achou este tutorial útil, compartilhe-o. E como sempre, estou disponível no Twitter em @adeelibr. Eu adoraria ouvir seus pensamentos sobre isso.