Olá, uma das coisas divertidas que podemos começas a brincar com o Deno é criando uma API e integrar ela com o MongoDB.
Aqui vai o passo a passo de como faremos isso, mas para isso vamos usar:
abc@v1.0.0-rc2 - For router(Similar Express)
dotenvc@v1.0.0-rc2 - Variables globals in root file
mongo@v0.6.0 - MongoDB connection
typescript@3.9 - Language typed
Agora que temos nossas libs vamos para a estrutura da nossa API.
Eu pensei em algo como:
- controllers -- Users - database - model - utils .env server.ts
SHOW ME THE CODE
Vamos começar configurando nosso server, crie um arquivo chamado server.ts na raiz do projeto e nesse arquivo a idéia é ser o mais simples possível e passar a abertura do servidor e os métodos rest, vamos analizar o código.
import { Application } from "https://deno.land/x/abc@v1.0.0-rc2/mod.ts"; const app = new Application(); app .get("/allusers", (c) => { return "Hello!"; .start({ port: 4000 }); console.log(`server listening on http://localhost:4000`);
Basicamente temos uma rota com retorno "Hello!" sem segredo.
Para executar, rode no seu CLI o comando:
deno run --allow-net server.ts
Se tudo der certo vai receber mensagem "server listening on http://localhost:4000" no console e então só bater nessa URL com o Postman, Insomnia, Postwoman ou qualquer outra aplicação do tipo =)
DB Connection
conexão com o banco de dados e pra isso vamos usar o Mongodb.com ele nos da a possibilidade de criar um banco gratuito com até 500mb de capacidade.
Criado o banco, vamos para o código e aqui vamo usar o TypeeScript, mas se não quiser tipagem só usar o .js e não colocar as tipagens, bem simples.
Dentro da pasta database
crie um arquivo chamado connections.ts
vamos concentrar nossa conexão aqui.
Feito isso vamos fazer o import das libs, abrir conexão e passar nossas variáveis de ambiente que serão DATABASE_NAME
e DATABASE_HOST
OBS. Estou utilizando a versão 0.6.0 porque a mais recente tive instabilidade para abrir conexão
import "https://deno.land/x/dotenv/load.ts"; import { init, MongoClient } from "https://deno.land/x/mongo@v0.6.0/mod.ts"; // @ts-ignore await init(); const dbName = Deno.env.get('DATABASE_NAME') || "deno"; const dbURI = Deno.env.get('DATABASE_HOST') || "mongodb://localhost:27017";
Aqui eu preferi trabalhar com conceito de class
pelo motivo de não ter que instanciar varias vezes em memória os valores das credenciais do banco e assim deixamos global.
class DataBase { public client: MongoClient; constructor( public dbName: string, public url: string ) { this.dbName = dbName; this.url = url; this.client = {} as MongoClient;; } connect() { const client = new MongoClient(); client.connectWithUri(this.url); this.client = client; }
Basicamente no código acima eu deixo exposto no método MongoClient
e no construtor eu passo o que preciso popular que no caso vão ser o dbName
e a url (connectionString)
Feito isso criamos um método chamado connect()
que será responsável pela abertura da conexão do mongo (como o próprio nome sugere)
Por fim terminamos nossa classe passando um método para encontrar a database que vamos trabalhar:
get findDatabase() { return this.client.database(this.dbName) } }
Onde basicamente vai nos retornar o database que pedimos
E para finalizar vamos fazer nossa chamada de classe e expor ela como default para nossos arquivos externos conseguirem enxergar
const connectionDatabase = new DataBase(dbName, dbURI); connectionDatabase.connect() export default connectionDatabase;
Feito isso você já poderá se conectar ao Mongo sem problemas.
Models
Antes de criar nossos métodos vamos preparar nossa tipagem de dados, mas para a lib do Mongo precisamos tipar e passar o $oid
e alguns dos métodos da controller.
export default interface User { _id: { $oid: string; }; name: string; middleName: string; profession: string; }
Handle Error
A ideia do Handler vai ser de normalizar o padrão de mensagens de erro na nossa aplicação
import { MiddlewareFunc } from "https://deno.land/x/abc@v1.0.0-rc2/mod.ts"; // Você já sabe como uma class funciona o/ export class ErrorHandler extends Error { status: number; constructor(msg: string, status: number) { super(msg); // Super é responsável por dizer que essa é uma classe pai no nível l da hierarquia this.status = status; } } export const ErrorMiddleware: MiddlewareFunc = (next) => async (data) => { try { // Se deu bom usamos o next() pra passar a info await next(data); } catch (err) { // Se deu ruim passamos no request só o que precisamos ver, mensagem e status code const error = err as ErrorHandler; data.response.status = error.status || 500; data.response.body = error.message; } };
Controllers
Nas controllers será onde colocaremos de fato a parte lógica da aplicação e aqui teremos arquivos para cada um dos métodos, achei melhor deixa-los separados para ficar fácil o manuseio, mas você pode deixar em um único arquivo os 4 métodos GET
,POST
,PUT
e DELETE
.
Vamos lá:
POST
Criaremos um arquivo chamado createUser.ts
dentro da pasta Users na controller vou explicar dentro do arquivo o que cada coisa faz:
// esses imports são para tipagem dos métodos de contexto e de trafego de informação pelas rotas da lib ABC import { HandlerFunc, Context, } from "https://deno.land/x/abc@v1.0.0-rc2/mod.ts"; // Importamos a nossa connectionDatabase para abriemos conexão com o banco de dados import connectionDatabase from "../../database/connection.ts"; // Importamos o handler de erros que criamos para termos um padrão na saída deles import { ErrorHandler } from "../../utils/handleError.ts"; // Abriremos nossa conexão passando a nossa collection previamente criada const database = connectionDatabase.findDatabase; const user = database.collection("users"); export const createUser: HandlerFunc = async (data: Context) => { // Vamos adotar a boa prática de passar um try...catch try { // Nesse primeiro if verificamos se nós estamos passando no Headers o content-type como application/json, se não estiver ele dispara um erro if (data.request.headers.get("content-type") !== "application/json") { throw new ErrorHandler("Body invalido", 422); } // Para pegar o que passamos no corpo da aplicação, vamos escrever uma request usando await de tudo que temos no data.body (data é o parâmetro dessa nossa função) const body = await (data.body()); // Esse nosso if usamos o Object.keys() para verificar se foi passado objetos no corpo da requisição if (!Object.keys(body).length) { throw new ErrorHandler("O body não pode estar vazio!!", 400); } // Feito essas validações vou desestruturar o que preciso inserir no banco const { name, profession, middleName } = body; // Depois usamos o método insertOne({}) do MongoDB para persistir esses dados await user.insertOne({ name, middleName, profession, }); // Terminamos aqui com o return passando uma mensagem que deu bom e o statusCode 200 o/ return data.json('Usuário cadastrado com sucesso', 201); } catch (error) { // Se der ruim nosso ErrorHandler será encarregado de nos avisar throw new ErrorHandler(error.message, error.status || 500); } };
GET
Criaremos um arquivo chamado getAllUsers.ts
dentro da pasta Users na controller vou explicar dentro do arquivo o que cada coisa faz.
O GET é o mais simples, pois só usaremos o find()
do mongo e retornaremos a lista de tudo que temos cadastrado
import { HandlerFunc, Context, } from "https://deno.land/x/abc@v1.0.0-rc2/mod.ts"; import connectionDatabase from "../../database/connection.ts"; import { ErrorHandler } from "../../utils/handleError.ts"; // Importamos a nossa tipagem de dados de usuário import Users from '../../model/users.ts' const database = connectionDatabase.findDatabase; const user = database.collection("users"); export const getAllUsers: HandlerFunc = async (data: Context) => { try { // Verificamos se existe o user com o método find() do Mongo const existUser: Users[] = await user.find(); // Se existir retornamos com o map(), se não retornamos array vazio if (existUser) { const list = existUser.length ? existUser.map((item: any) => { const { _id: { $oid }, name, middleName, profession } = item; console.log('item :>> ', item); return { id: $oid, name, middleName, profession }; }) : []; return data.json(list, 200); } } catch (error) { throw new ErrorHandler(error.message, error.status || 500); } };
GET ONE
Esse método será responsável por trazer apenas um registro que será passado por queryString
import { HandlerFunc, Context, } from "https://deno.land/x/abc@v1.0.0-rc2/mod.ts"; import connectionDatabase from "../../database/connection.ts"; import { ErrorHandler } from "../../utils/handleError.ts"; const database = connectionDatabase.findDatabase; const user = database.collection("users"); export const getUser: HandlerFunc = async (data: Context) => { try { // Vamos capturar o que será passado como parâmetro e usando o as do typescript para tipar o valor como string const { id } = data.params as { id: string }; // Usaremos o findOne() do mongo pra trazer uma bolleana de existUser ou não o valor const existUser = await user.findOne({ _id: { "$oid": id } }); if (existUser) { // Existindo, vamos desestruturar o resultado e retornar no data.json() junto com o status code const { _id: { $oid }, name, middleName, profession } = existUser; return data.json({ id: $oid, name, middleName, profession }, 200); } // Caso não exista, receberemos a mensagem abaixo com o status 404 throw new ErrorHandler("Usuário não encontrado", 404); } catch (error) { throw new ErrorHandler(error.message, error.status || 500); } };
PUT (UPDATE)
Criaremos um arquivo chamado updateUser.ts
dentro da pasta Users na controller vou explicar dentro do arquivo o que cada coisa faz.
Esse método tem mais validações por questão de segurança , se estaremos ou não passando as informações correta ou não, vamos analizar:
import { HandlerFunc, Context, } from "https://deno.land/x/abc@v1.0.0-rc2/mod.ts"; import connectionDatabase from "../../database/connection.ts"; import { ErrorHandler } from "../../utils/handleError.ts"; const database = connectionDatabase.findDatabase; const user = database.collection("users"); export const updateUser: HandlerFunc = async (data: Context) => { try { const { id } = data.params as { id: string }; if (data.request.headers.get("content-type") !== "application/json") { throw new ErrorHandler("Invalid body", 422); } // Capturamos o valor do corpo da aplicação e tipamos os dados passando o sinal de ? para sinalizar que é um campo opcional const body = await (data.body()) as { name?: string; middleName?: string; profession?: string; }; if (!Object.keys(body).length) { throw new ErrorHandler("O body não pode estar vazio!", 400); } // Aqui vamos usar o findOne() novamente e verificar se existe const existUser = await user.findOne({ _id: { "$oid": id } }); if (existUser) { const { matchedCount } = await user.updateOne( { _id: { "$oid": id } }, { $set: body }, ); // Esse matchedCount que recebemos no resultado do findOne() é o responsável por nos dar a informação de se foi encontrado ou não no caso será 1 dado e finalizamos com return data.string() if (matchedCount) { return data.string("O usuário foi atualizado com sucesso!", 204); } // Caso não der certo mandaremos essa mensagem para a requisição return data.string("Não foi possível atualizar esse usuário"); } throw new ErrorHandler("Usuário não encontrado", 404); } catch (error) { throw new ErrorHandler(error.message, error.status || 500); } };
DELETE
Criaremos um arquivo chamado deleteUser.ts
dentro da pasta Users na controller vou explicar dentro do arquivo o que cada coisa faz.
O nosso delete é bem simples com a carga das coisas que aprendemos nos anteriores, veja:
import { HandlerFunc, Context, } from "https://deno.land/x/abc@v1.0.0-rc2/mod.ts"; import connectionDatabase from "../../database/connection.ts"; import { ErrorHandler } from "../../utils/handleError.ts"; const database = connectionDatabase.findDatabase; const user = database.collection("users"); export const deleteUser: HandlerFunc = async (data: Context) => { try { const { id } = data.params as { id: string }; const existUser = await user.findOne({ _id: { "$oid": id } }); if (existUser) { // Usaremos o deleteOne() do banco para realizar a operação e ele vai retornar o valor de dado deletado, no caso 1 const deleteCount = await user.deleteOne({ _id: { "$oid": id } }); if (deleteCount) { // Após receber o valor 1 vamos responder a requisição com a mensagem abaixo return data.string("Usuário foi deletado!", 204); } throw new ErrorHandler("Não foi possivel excuir esse usuário", 400); } throw new ErrorHandler("Usuário não encontrado", 404); } catch (error) { throw new ErrorHandler(error.message, error.status || 500); } };
Por fim vamos padronizar as exportações criando um arquivo index.ts dentro da pasta Users com os seguinte código:
export { getAllUsers } from './getAllUsers.ts'; export { createUser } from './createUser.ts'; export { getUser } from './getOneUser.ts'; export { updateUser } from './updateUser.ts'; export { deleteUser } from './deleteUser.ts';
.ENV
O .ENV é onde vamos colocar as variáveis globais da aplicação
Crie um arquivo chamado .env na raiz do projeto (mesmo nivel do arquivo server.ts) com a seguinte informação:
DATABASE_NAME=deno DATABASE_HOST=<_sua_url_do_mongo_>
Foi muita coisa agora, mas agora você sabe criar uma API para brincar nos seus projetos ou até mesmo aplicar com NodeJS
Feito tudo isso podemos executar nosso projeto com o comando
deno run --allow-write --allow-read --allow-plugin --allow-net --allow-env --unstable ./server.ts
O código dessa API se encontra nesse Repositório
Por enquanto é isso e nos vemos em breve, dúvidas ou sugestão deixem nos comentários ou nos procure nas redes Sociais!
Acompanhe nossos canais de conteúdo:
Top comments (1)
Só uma pergunta. A segurança do deno esta nas flags que vc passa na hora de executar o código?