Pular para o conteúdo principal

Criando sua primeira API REST com Deno e Postgres


Criado pelas mesmas mentes por trás do Node.js, o Deno está ganhando força entre os desenvolvedores.

Depois de maturar por um tempo e evoluir features que o Node falhou em oferecer, como segurança, módulos e dependências, Deno vem provando ser tão poderoso quanto seu antecessor.

Estruturalmente, ele é basicamente uma runtime TypeScript construída sobre o robusto Google V8 Engine. Mas não se preocupe, o Deno também oferece suporte a JavaScript vanilla, que é o que usaremos neste artigo.

O Deno foi criado sob algumas condições:
  • Em primeiro lugar, é seguro, o que significa que sua execução padrão é baseada em um ambiente sandbox.
  • Por padrão, não há acesso liberado por padrão a recursos como rede, sistema de arquivos, etc. Quando seu código tentar acessar tais recursos, será solicitada a respectiva permissão.
  • Ele carrega módulos por URLs (como os navegadores). Isso permite que você use código descentralizado como módulos e importe-os diretamente em seu código-fonte, sem ter que se preocupar com os famosos registries.
  • Também é compatível com o navegador. Por exemplo, se você estiver usando módulos ES, não precisa se preocupar com o uso de Webpack ou Gulp.
Além disso, é baseado em TypeScript.

Se você já trabalha com o TypeScript não há necessidade de configurações extras. Caso contrário,  também poderá usá-lo com JavaScript puro.

Você pode ler mais sobre isso aqui e em sua documentação oficial.

Neste artigo, vamos nos concentrar mais no how-to.

Especificamente, veremos como criar uma API do zero usando apenas JavaScript, Deno e uma conexão com um banco de dados Postgres.

O aplicativo que iremos desenvolver é um CRUD básico sobre um domínio de cervejas. Simbora!

Setup

Primeiro, você precisa ter as ferramentas e tudo configurado. Para este artigo, precisaremos de:
  • Uma IDE de sua escolha - usaremos o VS Code.
  • Um servidor Postgres e sua ferramenta GUI favorita para gerenciá-lo.
  • Deno.
Instalar o Deno é muito simples. No entanto, como o Deno está constantemente sendo atualizado, nos concentraremos na versão 1.0.0, para que sempre funcione independentemente dos novos recursos lançados.

Para isso, execute o seguinte comando:
// No Shell
curl -fsSL https://deno.land/x/install/install.sh | sh -s v1.0.0

// No PowerShell
$v="1.0.0"; iwr https://deno.land/x/install/install.ps1 -useb | iex
Em seguida, execute o comando deno --version para checar se a instalação foi bem-sucedida.

Você verá como resultado:
deno 1.0.0
v8 8.4.300
typescript 3.9.2

Estrutura do Projeto

A seguir, vamos criar a estrutura do projeto, incluindo arquivos e pastas iniciais. Dentro de uma pasta de sua preferência, replique a mesma estrutura da imagem abaixo:


A estrutura pode ser entendida da seguinte forma:
  • controllers: contêm os arquivos JS que irão tratar os requests que chegam, as chamadas posteriores aos serviços e camadas inferiores e, por fim, a devolução das respostas. Todos esses objetos são herdados do Deno, então você não precisa se preocupar se precisará lidar com requests/responses manualmente.
  • db: a pasta que hospeda nosso script SQL de criação e a conexão direta com o banco de dados Postgres.
  • repositories: esses arquivos JS irão lidar com o gerenciamento das operações do banco de dados. Cada criação, exclusão ou atualização ocorrerá aqui.
  • services: são os arquivos que tratam da lógica de negócios de nossas operações, como validações, transformações sobre os dados, etc.

A Aplicação

Comecemos com o código do nosso primeiro e mais importante arquivo, index.js:
import { Application } from "https://deno.land/x/oak/mod.ts";
import { APP_HOST, APP_PORT } from "./config.js";
import router from "./routes.js";
import _404 from "./controllers/404.js";
import errorHandler from "./controllers/errorHandler.js";

const app = new Application();

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

console.log(`Listening on port:${APP_PORT}...`);

await app.listen(`${APP_HOST}:${APP_PORT}`);

Precisamos de um framework web para lidar com os detalhes do tratamento dos requests e responses, gerenciamento de threads, erros, etc.

Para o Node, é comum usar Express ou Koa para essa finalidade.

No entanto, como vimos, Deno não oferece suporte a bibliotecas Node.

Precisamos usar outra opção inspirada no Koa, o Oak: um framework middleware para o servidor net do Deno. Observe que estamos usando a versão 4.0.0 do Oak. Esta é a versão certa para o Deno 1.0.0. Quando você não fornece uma versão, o Deno sempre busca a mais recente, o que pode causar alterações significativas no funcionamento do seu código. Por isso, tome cuidado!

O Oak tem uma estrutura de middleware inspirada no Koa, e seu router foi inspirado no koa-router.

Seu uso é muito semelhante ao Express. Na primeira linha, estamos importando o módulo TS diretamente da URL deno.land.

O restante dos imports será configurado posteriormente.

A classe Application é onde tudo começa no Oak.

Nós a instanciamos e adicionamos o error handler, os controllers, o sistema de roteamento e, por fim, chamamos o método listen() para iniciar o servidor passando a URL (host + porta).

Aqui você pode ver o código do config.js (coloque-o na raiz do projeto):
export const APP_HOST = Deno.env.get("APP_HOST") || "127.0.0.1";
export const APP_PORT = Deno.env.get("APP_PORT") || 4000;

Muito familiar, não é mesmo? Vamos para as rotas agora.

Como no Express, precisamos estabelecer os routers que redirecionarão nossos requests para as funções JavaScript adequadas que, por sua vez, irão tratá-las, armazenando ou pesquisando dados e, por fim, retornando os resultados.

Dê uma olhada no código do routes.js (também na pasta raiz):
import { Application } from "https://deno.land/x/oak@v4.0.0/mod.ts";
import getBeers from "./controllers/getBeers.js";
import getBeerDetails from "./controllers/getBeerDetails.js";
import createBeer from "./controllers/createBeer.js";
import updateBeer from "./controllers/updateBeer.js";
import deleteBeer from "./controllers/deleteBeer.js";

const router = new Router();

router
  .get("/beers", getBeers)
  .get("/beers/:id", getBeerDetails)
  .post("/beers", createBeer)
  .put("/beers/:id", updateBeer)
  .delete("/beers/:id", deleteBeer);

export default router;

Até agora, nada deve estar funcionando ainda. Não se preocupe — ainda precisamos configurar o resto do projeto antes de iniciá-lo.

Este último código mostra que o Oak também cuidará do sistema de rotas para nós.

A classe Router, mais especificamente, será instanciada para permitir o uso dos métodos correspondentes para cada operação HTTP GET, POST, PUT e DELETE.

Os imports no início do arquivo correspondem a cada uma das funções que tratarão o respectivo request.

Você pode decidir se prefere desta forma ou se prefere ter tudo no mesmo controller.

Banco de dados e repositório

Antes de prosseguirmos, precisamos configurar o banco de dados.

Certifique-se de ter o Postgres server instalado e funcionando no localhost. Conecte-se a ele e crie um novo banco de dados chamado logrocket_deno.

Em seguida, entre no banco e, no esquema public, execute o seguinte script de criação:
CREATE TABLE IF NOT EXISTS beers (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    brand VARCHAR(50) NOT NULL,
    is_premium BOOLEAN,
    registration_date TIMESTAMP
)

Este script também está disponível na pasta /db da minha versão do projeto.

Ele cria uma nova tabela, “beers”, para armazenar os valores de nosso CRUD.

Observe que a chave primária é auto-incrementada (via palavra-chave SERIAL) para facilitar nosso trabalho com a estratégia de geração de id.

Agora, vamos criar o arquivo que tratará da conexão com o Postgres.

Na pasta db, crie o arquivo database.js e adicione o seguinte conteúdo:
import { Client } from "https://deno.land/x/postgres/mod.ts";

class Database {
  constructor() {
    this.connect();
  }

  async connect() {
   this.client = new Client({
      user: "postgres",
      database: "logrocket_deno",
      hostname: "localhost",
      password: "postgres",
      port: 5432
    });

    await this.client.connect();
  }
}

export default new Database().client;

Certifique-se de ajustar as configurações de conexão de acordo com as configurações do Postgres. A configuração é bem simples.

Deno criou seu deno-postgres (driver PostgreSQL para Deno) baseado nos módulos node-postgres e pg.

Se você já usa Node, deve estar familiarizado com a sintaxe.

Esteja ciente de que as configurações mudam ligeiramente dependendo do banco de dados que você usa.

Aqui, estamos passando o objeto de configuração como um parâmetro Client.

No MySQL, entretanto, ele vai diretamente via função connect().

Dentro da pasta repositories, vamos criar o arquivo beerRepo.js, que hospedará os repositórios para acessar o banco de dados por meio do arquivo que vimos acima.

Este é o seu código:
import client from "../db/database.js";

class BeerRepo {
  create(beer) {
    return client.query(
      "INSERT INTO beers (name, brand, is_premium, registration_date) VALUES ($1, $2, $3, $4)",
      beer.name,
      beer.brand,
      beer.is_premium,
      beer.registration_date
    );
  }

  selectAll() {
    return client.query("SELECT * FROM beers ORDER BY id");
  }

  selectById(id) {
    return client.query(`SELECT * FROM beers WHERE id = $1`, id);
  }
  
  update(id, beer) {
    var latestBeer = this.selectById(id);
    var query = `UPDATE beers SET name = $1, brand = $2, is_premium = $3 WHERE id = $4`;

    return client.query(query,
        beer.name !== undefined ? beer.name : latestBeer.name,
        beer.brand !== undefined ? beer.brand : latestBeer.brand,
        beer.is_premium !== undefined ? beer.is_premium : latestBeer.is_premium, id);
  }
}

export default new BeerRepo();

Importe o arquivo database.js que se conecta ao banco de dados. O resto do arquivo representa apenas um apunhado de operações CRUD. Vá em frente e dê uma boa olhada nelas.

Para evitar SQL injection — como outros frameworks de banco de dados — o Deno nos permite passar parâmetros para nossas consultas SQL.

Novamente, cada banco de dados tem sua própria sintaxe.

Com o Postgres, por exemplo, usamos o cifrão seguido pelo número do parâmetro em sua ordem específica.

A ordem aqui é muito importante. No MySQL, o operador é um ponto de interrogação (?).

Os valores de cada parâmetro vêm depois, como um parâmetro varargs (no Postgres; para MySQL, seria um array).

Cada item deve estar exatamente na mesma posição do seu operador de consulta correspondente.

A função query() é aquela que usaremos toda vez que quisermos acessar ou alterar dados no banco de dados.

Já no método de update, usamos uma das muitas estratégias possíveis para atualizar apenas o que o usuário está passando. Primeiro, buscamos o usuário pelo id e o armazenamos em uma variável local. Então, ao verificar se cada atributo de cerveja está dentro payload do request, podemos decidir se usaremos ele ou o valor armazenado.

Serviços

Agora, vamos passar para a camada de serviços. Dentro da pasta services, crie o arquivo beerService.js e adicione o seguinte código:
import beerRepo from "../repositories/beerRepo.js";

export const getBeers = async () => {
  const beers = await beerRepo.selectAll();

  var result = new Array();

  beers.rows.map(beer => {
    var obj = new Object();

    beers.rowDescription.columns.map((el, i) => {
      obj[el.name] = beer[i];
    });
    result.push(obj);
  });

  return result;
};

export const getBeer = async beerId => {
  const beers = await beerRepo.selectById(beerId);

  var obj = new Object();
  beers.rows.map(beer => {
    beers.rowDescription.columns.map((el, i) => {
      obj[el.name] = beer[i];
    });
  });

  return obj;
};

export const createBeer = async beerData => {
  const newBeer = {
    name: String(beerData.name),
    brand: String(beerData.brand),
    is_premium: "is_premium" in beerData ? Boolean(beerData.is_premium) : false,
    registration_date: new Date()
  };

  await beerRepo.create(newBeer);

  return newBeer.id;
};

export const updateBeer = async (beerId, beerData) => {
  const beer = await getBeer(beerId);

  if (Object.keys(beer).length === 0 && beer.constructor === Object) {
    throw new Error("Beer not found");
  }

  const updatedBeer = {
    name: beerData.name !== undefined ? String(beerData.name) : beer.name,
    brand: beerData.brand !== undefined ? String(beerData.brand) : beer.brand,
    is_premium:
      beerData.is_premium !== undefined
        ? Boolean(beerData.is_premium)
        : beer.is_premium
  };

  beerRepo.update(beerId, updatedBeer);
};

export const deleteBeer = async beerId => {
  beerRepo.delete(beerId);
};

Este é um dos arquivos mais importantes. É aqui que interagimos diretamente com o repositório, bem como sofremos chamadas dos controllers.

Cada método também corresponde a uma das operações CRUD e, como a natureza das operações com banco de dados no Deno é inerentemente assíncrona, ele sempre retorna uma promise.

É por isso que precisamos fazer uso extensivo do await.

Além disso, o retorno é um objeto que não corresponde exatamente ao nosso objeto de negócios, Beer, portanto, temos que transformá-lo em um objeto JSON compreensível.

A função getBeers sempre retornará um array e getBeer um único objeto.

A estrutura de ambas as funções é muito semelhante.

O object beers é um array de arrays porque encapsula uma lista de possíveis retornos para nossa consulta, e cada retorno também é um array (dado que cada valor de coluna vem dentro deste array).

rowDescription, por sua vez, armazena as informações (incluindo os nomes) de cada coluna que os resultados possuem.

Alguns outros recursos, como validações, também ocorrem aqui.

Na função updateBeer, você pode ver que estamos sempre verificando se o beerId fornecido de fato existe no banco de dados antes de prosseguir com a atualização.

Caso contrário, um erro será gerado. 

Os Controllers 

Agora é hora de criar os gerenciadores dos nossas requests e responses.

Validações de entrada e saída aderem melhor a esta camada.

Vamos começar com os arquivos de gerenciamento de erros — aqueles que vimos no index.js.

Na pasta controllers, crie os arquivos 404.js e errorHandler.js.

Código para o 404.js:
export default ({ response }) => {
  response.status = 404;
  response.body = { msg: "Not Found" };
};
Código para o errorHandler.js:
export default async ({ response }, nextFn) => {
  try {
    await nextFn();
  } catch (err) {
    response.status = 500;
    response.body = { msg: err.message };
  }
};

No primeiro, estamos apenas exportando uma função que cuidará das exceções de negócio sempre que as lançarmos, como HTTP 404.

O segundo cuidará de qualquer outro tipo de erro desconhecido que possa ocorrer no ciclo de vida da aplicação, tratando-os como HTTP 500 e enviando a mensagem de erro no corpo da resposta.

Agora, vamos aos controllers, começando com os getters.

Este é o conteúdo de getBeers.js:
import { getBeers } from "../services/beerService.js";

export default async ({ response }) => {
  response.body = await getBeers();
};

Cada operação no controller deve ser assíncrona. Cada uma recebe um ou ambos os objetos de request e response como parâmetros.

Eles são interceptados pela API do Oak e pré-processados antes de chegar ao controller ou retornar ao client.

Independentemente do tipo de lógica que você colocar lá, não se esqueça de definir o corpo da resposta, pois esse é o resultado do seu request.

A seguir está o conteúdo de getBeerDetails.js:
import { getBeer } from "../services/beerService.js";

export default async ({
  params,
  response
}) => {
  const beerId = params.id;

  if (!beerId) {
    response.status = 400;
    response.body = { msg: "Invalid beer id" };
    return;
  }

  const foundBeer = await getBeer(beerId);
  if (!foundBeer) {
    response.status = 404;
    response.body = { msg: `Beer with ID ${beerId} not found` };
    return;
  }

  response.body = foundBeer;
};

Este código é semelhante ao do getbeers.js, exceto pelas validações.

Como estamos recebendo o beerId como parâmetro, é bom verificar se ele está preenchido. Se o valor para esse parâmetro não existir, envie uma mensagem correspondente no corpo.

A próxima etapa é a criação do arquivo.

Este é o conteúdo do arquivo createBeer.js:
import { createBeer } from "../services/beerService.js";

export default async ({ request, response }) => {
  if (!request.hasBody) {
    response.status = 400;
    response.body = { msg: "Invalid beer data" };
    return;
  }

  const {
    value: { name, brand, is_premium }
  } = await request.body();

  if (!name || !brand) {
    response.status = 422;
    response.body = { msg: "Incorrect beer data. Name and brand are required" };
    return;
  }

  const beerId = await createBeer({ name, brand, is_premium });

  response.body = { msg: "Beer created", beerId };
};

Novamente, algumas validações ocorrem para garantir que os dados de entrada são válidos em relação aos campos obrigatórios. As validações também confirmam que um corpo vem com a solicitação.

A chamada para a função createBeer passa cada argumento individualmente. Se o objeto cerveja aumentar em seu número de atributos, não seria sensato manter tal função.

Em vez disso, você pode criar um objeto model, que armazenaria cada um dos atributos da sua cerveja e seria passado para os controllers e métodos de serviço.

Este é o nosso updateBeer.js:
import { updateBeer } from "../services/beerService.js";

export default async ({ params, request, response }) => {
  const beerId = params.id;

  if (!beerId) {
    response.status = 400;
    response.body = { msg: "Invalid beer id" };
    return;
  }

  if (!request.hasBody) {
    response.status = 400;
    response.body = { msg: "Invalid beer data" };
    return;
  }

  const {
    value: { name, brand, is_premium }
  } = await request.body();

  await updateBeer(beerId, { name, brand, is_premium });

  response.body = { msg: "Beer updated" };
};

Como você pode ver, tem quase a mesma estrutura. A diferença está na configuração dos parâmetros.

Como não estamos permitindo que todos os atributos de uma cerveja sejam atualizados, limitamos aqueles que irão para a camada de serviço.

O beerId também deve ser o primeiro argumento, pois precisamos identificar qual elemento do banco de dados atualizar.

E, finalmente, o código para o deleteBeer.js:
import { deleteBeer, getBeer } from "../services/beerService.js";

export default async ({
  params,
  response
}) => {
  const beerId = params.id;

  if (!beerId) {
    response.status = 400;
    response.body = { msg: "Invalid beer id" };
    return;
  }

  const foundBeer = await getBeer(beerId);
  if (!foundBeer) {
    response.status = 404;
    response.body = { msg: `Beer with ID ${beerId} not found` };
    return;
  }

  await deleteBeer(beerId);
  response.body = { msg: "Beer deleted" };
};

Observe como ele é semelhante aos outros.

Novamente, se você achar que é muito repetitivo, poderá centralizar os códigos dos controllers em um único arquivo.

Isso permitiria menos código, já que o código comum a todos eles estaria junto em uma função, por exemplo.

Agora vamos testar. Para executar o projeto, na linha de comando, execute o seguinte comando:
deno run --allow-net --allow-env index.js

Lembre-se que o Deno funciona com recursos seguros. Isso significa que para permitir chamadas HTTP e acessar variáveis env, precisamos pedir isso explicitamente. Essas flags servem para isso.

Os logs mostrarão o Deno baixando todas as dependências que o projeto precisa. A mensagem “Listening on port: 4000...” deve aparecer em seguida.

Para testar a API, usaremos o Postman. Este é o exemplo de um POST em ação:

Depois, vamos listar todas as cervejas do banco de dados via GET:

Conclusão

Vou deixar o resto dos testes com você. O código final para este tutorial pode ser encontrado aqui.

Observe que concluímos uma API de CRUD completa e funcional sem Node.js ou um diretório node_modules (já que o Deno mantém as dependências em cache).

Toda vez que você quiser usar uma dependência, apenas declare-a no código e o Deno fará o download (não há necessidade de um arquivo package.json).

Bons estudos!
Esse artigo é uma versão traduzida do meu artigo "Creating your first REST API with Deno and Postgres" na LogRocket.

Comentários

Postagens mais visitadas deste blog

Integrando Android e PayPal com Java e MySQL - Parte 1

Uma das funcionalidades mais importantes da maioria dos apps mobile de hoje em dia é a possibilidade de se integrar com plataformas de pagamento online, tais como PagSeguro, MercadoPago ou a mais famosa de todas (a nível internacional): PayPal . Na maioria dos apps de eCommerce, que se caracterizam principalmente por compras e vendas online, não basta somente ter uma boa interface e lógica de negócio implementadas, é também preciso gerenciar tudo isso de forma segura, e para isso precisamos fazer uso de Web Services, bancos de dados, aplicações e outros tipos de recursos e operações no lado do servidor. Com o objetivo de cobrir uma implementação pouco vista em português, esse artigo, dividido em duas partes, visa ensinar como construir uma aplicação básica em Android , usando a biblioteca SDK do PayPal , uma integração server side com um projeto em Java Web , que fará uso de requisições HTTP via Web Services Restful (implementação Jersey ) e salvará os dados em um schema MySQ

Como acessar um iframe e seus elementos via jQuery?

Recentemente tive  um problema no projeto pois sentiu a necessidade de acessar um valor de um input que estava dentro de um iframe. Esse tipo de situação não é tão comum, uma vez que geralmente acessamos os valores do iframe para fora. Para acessar, de dentro de um iframe, um valor externo, utilizamos o seguinte código: $('#idDoElementoExterno', parent.document).val(); Entretanto, nunca tínhamos passado pela situação contrária. Pesquisando um pouco descobrimos uma alternativa, porém em JavaScript. Para ficar melhor o entendimento, vamos simular uma situação aqui. Temos uma página html "A.html" e dentro da mesma existe um iframe que aponta (src) para uma página "B.html": <!-- A.html --> <html> <head> <title>Testando iframe - jQuery</title> <script language="JavaScript"> function exibeValor() { // alert aqui! } </script> </head> <body> <input typ

"Content is not allowed in prolog" - Entendendo exceção no Seam

Recentemente tive um problema de edição em um arquivo .xhtml utilizando JBoss Seam, Richfaces e afins. A princípio a mensagem de erro não dizia muito a respeito da causa do mesmo: com.sun.facelets.FaceletException: Error Parsing /consulta.xhtml: Error Traced[line: 1] Content is not allowed in prolog. "O conteúdo não é permitido no prólogo". Mas que conteúdo? Em qual prolog? Depois de dar uma pesquisada descobri que o erro acontece em vista de terem sido colocados alguns caraceteres inválidos antes da declaração de documento xml na página xhtml. Em outras palavras, a primeira coisa que deve constar em um documento xml (afins) deve ser: <?xml version="1.0" encoding="utf-8"?> Qualquer coisa antes disso, até mesmo um simples espaço em branco, pode gerar o erro em questão. Por fim, lembre-se de que a declaração de documento xml segue o padrão de encoding definido. Logo temos: <!-- Inc