Criando API com DENO e MongoBD
Telles (@UnicornCoder)
Posted on June 18, 2020
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:
Posted on June 18, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.