Criando API com DENO e MongoBD

telles

Telles (@UnicornCoder)

Posted on June 18, 2020

Criando API com DENO e MongoBD

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
Enter fullscreen mode Exit fullscreen mode

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`);

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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;
    }
  };
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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'; 
Enter fullscreen mode Exit fullscreen mode

.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_>

Enter fullscreen mode Exit fullscreen mode

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:

💖 💪 🙅 🚩
telles
Telles (@UnicornCoder)

Posted on June 18, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related