DEV Community

Cover image for Construindo uma API com Deno: Da instalação ao Deploy
Guilherme Camargo
Guilherme Camargo

Posted on

Construindo uma API com Deno: Da instalação ao Deploy

Criei minha primeira API com Deno recentemente e decidi compartilhar tudo que aprendi sobre ele neste artigo.

Esta API é parte de um projeto open-source que estou desenvolvendo. Meu objetivo com ele é implementar um "e-commerce" seguindo a arquitetura de microservices, com a stack JS, e aprender outras tecnologias no processo. Todo o código que usei está no meu GitHub.

O que é o Deno

Deno como todos já estão cansados de ouvir é o novo runtime JS/TS, feito pelo mesmo criador do Node, Ryan Dahl. Ele possui fortes restrições de segurança, é construído com a linguagem Rust, em cima da V8 do Google e tem uma meia um dinossauro de mascote.

logo

Instalação

Vou sair um pouco do convencional e mostrar uma alternativa pra instalar o Deno - ou qualquer outro runtime que queira.

asdf é um gerenciador de versão universal. Após instalá-lo com o guia oficial rode os seguintes comandos:

$ asdf plugin-add deno https://github.com/asdf-community/asdf-deno.git
$ asdf install deno 1.0.5
$ asdf global deno 1.0.5
Enter fullscreen mode Exit fullscreen mode

Como estou usando a versão 1.0.5 nos exemplos, deixei fixo aqui, mas você pode repetir os últimos dois comandos com "latest" e usar a versão mais atual depois.

Para conferir se deu tudo certo é só rodar $ deno --version.

Setup Environment

VS Code extension

VS Code se tornou o ambiente padrão para muitas linguagens, acredito que JS/TS seja a maior delas. Já temos uma extensão criada pela equipe do Deno que nos dará um suporte melhor durante o desenvolvimento.

VS Code settings

Após instalar a extensão do Deno precisamos setar a versão do Typescript que o VS Code usa para a última versão. Se ainda não instalou - e considerando que você já tenha Node instalado na sua máquina - rode $ npm i -g typescript. Agora precisamos obter o caminho para os executáveis. No terminal rode $ npm list -g typescript, ele irá nos dizer o caminho até a pasta raiz onde o TS foi instalado, navegue até lá e vá para a pasta "lib", rode $ pwd, copie todo o caminho e cole na propriedade "typescript.tsdk" no settings.json global do VS Code ou do workspace.

No meu caso estou usando o settings do workspace com algumas outras configurações, recomendo usar as mesmas.

{
  "typescript.tsdk": "~/.asdf/installs/nodejs/12.18.0/.npm/lib/node_modules/typescript/lib",
  "[typescript]": {
    "editor.defaultFormatter": "vscode.typescript-language-features",
  },
  "deno.enable": true,
  "deno.unstable": true,
}
Enter fullscreen mode Exit fullscreen mode

Variáveis de ambiente

O Deno usa uma pasta global na sua máquina para salvar algumas coisas. Caso não tenha sido criada ainda rode $ mkdir -p ~/.deno/bin. Este será o caminho onde ficará alguns executáveis que instalaremos com o Deno. Agora precisamos que nosso terminal olhe para esta pasta quando for procurar pelos binários. Abra o arquivo de configuração do seu terminal (.bashrc, .zshrc, ...) e cole isso no início do arquivo.

export DENO_DIR=$HOME/.deno
export PATH=$PATH:$DENO_DIR/bin
Enter fullscreen mode Exit fullscreen mode

Recomendo que feche e abra uma nova instância do terminal para que reflita as alterações.

Typescript

O projeto será full typescript. Você pode usar JS puro se preferir, mas prefiro TS, gosto muito do tooling que ele me dá e ele me ajuda a evitar erros bobos. Além disso, TS é a linguagem recomendada para desenvolver com Deno.

Apesar de não ser necessário geralmente, neste projeto iremos utilizar decorators e para isso precisamos dizer ao TS para aceitar essa sintaxe. Para isso temos que criar o arquivo tsconfig.json dentro da pasta src.

{
    "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}
Enter fullscreen mode Exit fullscreen mode

Estrutura do projeto

Esta API será responsável por cadastrar as avaliações dos usuários para os produtos que eles comprarem.

Só teremos duas rotas:

  • POST /ratings
  • GET /ratings

A estrutura final do projeto será esta:

.
├── .env
├── .env.example
├── .vscode
│   ├── launch.json
│   └── settings.json
├── Dockerfile
├── denon.json
├── docker-compose.yml
└── src
    ├── controllers
    │   └── rating.controller.ts
    ├── database
    │   ├── connection.ts
    │   ├── entities
    │   │   ├── buyer.entity.ts
    │   │   ├── product.entity.ts
    │   │   └── rating.entity.ts
    │   └── repositories
    │       ├── buyer.repository.ts
    │       ├── product.repository.ts
    │       └── rating.repository.ts
    ├── deps.ts
    ├── env.ts
    ├── routes.ts
    ├── server.ts
    └── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Framework Web

O Deno tem uma ótima standard library, porém não queremos trabalhar com a classe de http diretamente, pois teríamos que fazer muita coisa na mão, é por isso que usamos frameworks. No Node o comum é o express, no Deno o framework mais estável e com mais uso até o momento é o oak - ele é baseado no koa pessoal do Deno adoram fazer anagramas.

A primeira coisa que precisamos criar é o arquivo server.ts, que será o arquivo de entrada para a aplicação.

import { Application } from 'https://deno.land/x/oak/mod.ts'

const app = new Application();
const port = 8001

console.log(`Running on port ${port}`);
await app.listen({ port: port});
Enter fullscreen mode Exit fullscreen mode

Agora podemos rodar usando $ deno run --allow-net server.ts.

Imports

A primeira coisa que vemos de diferente em comparação com o Node é que no Deno usamos ES Modules ao invés do CommonJS e estamos dando import de uma URL direto no nosso código. Isso pode parecer meio estranho - Deno se inspirou na linguagem Go neste aspecto - porém no browser isso sempre foi comum. É como a tag script do HTML funciona: <script src="URL">

node_modules / package.json

  • Então se estamos importando as dependências via URL, toda vez que quisermos rodar a aplicação teremos de ter acesso à internet?
  • Não!

Assim que usamos uma nova dependência o Deno baixa o código desta URL para um cache em uma pasta global na sua máquina - o caminho padrão é ~/.deno/deps/https. Na próxima vez que rodarmos esta aplicação, ou outra que tenha a mesma dependência, o Deno irá obter a dependência desta pasta.

Dito isso, não precisamos mais de uma pasta na raiz do nosso projeto como o node_modules. Se, por exemplo, temos duas APIs com Deno que usam a mesma versão do oak, não teremos ele duplicado duas vezes na nossa máquina.

Como não instalamos as dependências, só fazemos referência, também não precisamos mais do arquivo package.json. A única coisa que precisamos é do próprio arquivo JS/TS.

deps.ts

O ponto ruim de fazer referência via URL é que, dependendo do tamanho do projeto, você ficará com URLs espalhadas por toda a aplicação. E caso queira atualizar uma dependência para outra versão, remover ou até mesmo saber quais libs estão sendo usadas, você não terá isso em um ponto centralizado.

Pensando nisso podemos centralizar essas referências em um arquivo e importar o que precisamos deste arquivo.

Dentro de src crie o arquivo deps.ts

export { 
  Application,
  Router,
  Context
} from "https://deno.land/x/oak/mod.ts"
Enter fullscreen mode Exit fullscreen mode

Top level await

Outra coisa interessante que estamos usando é await sem uma função async. Deno tem esta feature graças ao V8, onde podemos usar await direto na raiz do arquivo.

Denon

Assim como no Node usamos o Nodemon para que a aplicação reinicie toda vez que um arquivo é alterado, no Deno usamos o denon. Após instalá-lo seguindo o readme, vamos criar um arquivo chamado denon.json na raiz do projeto. Ele é muito útil, podemos fazer varias configurações de como nossa aplicação deve rodar.

{
  "$schema": "https://deno.land/x/denon/schema.json",
  "allow": [
    "env",
    "net"
  ],
  "tsconfig": "src/tsconfig.json",
  "unstable": true,
  "scripts": {
    "start": {
      "cmd": "src/server.ts"
      "env": {
        "APP_PORT": "8001",
      }
    }
  },
  "logger": {
    "debug": true
  }
}
Enter fullscreen mode Exit fullscreen mode
  • allow é onde damos as permissões do que o Deno pode executar na nossa máquina.
  • tsconfig é pra dizermos pro Deno considerar nossas configurações extras do TS;
  • unstable pois nem todas as funcionalidades do deno e de algumas libs estão prontas;
  • scripts é onde criamos configurações específicas. Podemos dar qualquer nome, coloquei start por convenção mesmo;
  • cmd temos só nome do arquivo de entry-point para aplicação, mas ele irá executar $ deno run... antes;
  • env para exportar variáveis de ambientes que só esta aplicação irá usar;
  • logger para termos um log mais detalhado no terminal;

Assim que rodarmos $ denon start você notará que o que ele realmente executa é $ deno run --allow-env --allow-net --config src/tsconfig.json --unstable src/server.ts. Imagina ter que lembrar e digitar tudo isso toda vez? Denon é opcional, mas extremamente útil.

Debuging

Pra melhorar ainda mais nossa experiência com Deno podemos debugar a aplicação pelo VS Code.

Basta criar o arquivo .vscode/launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Deno",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "deno",
      "runtimeArgs": [
        "run",
        "--unstable",
        "--config",
        "src/tsconfig.json",
        "--inspect-brk",
        "-A",
        "src/server.ts"
      ],
      "port": 9229
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Camada de dados

TypeORM

Acho ORM uma ferramenta indispensável. Trabalhar direto com SQL no código - além de deixar tudo mais sujo, extenso e complexo - abre brechas de segurança e possibilidade de vários bugs. ORMs encapsulam toda essa complexidade e nos oferecem uma interface simples, testada e a possibilidade de usar a mesma estrutura da linguagem com o restante da aplicação.

Não foi fácil encontrar um ORM que funcionasse direito no Deno, testei alguns e o que acabei mais gostando foi um fork do TypeORM que já existia no Node.

Dentro do arquivo deps.ts vamos adicionar esta nova dependência

export { 
  createConnection, 
  Connection, 
  Entity, 
  PrimaryGeneratedColumn, 
  Column, 
  OneToMany,
  ManyToOne,
  Repository
} from "https://denolib.com/denolib/typeorm@v0.2.23-rc4/mod.ts";
Enter fullscreen mode Exit fullscreen mode

Até o momento da publicação deste artigo este fork está em release candidate ainda, muitas coisas podem não funcionar corretamente. Paciência você deve ter.

yoda

Entities

A primeira coisa que devemos fazer é criar as entidades. Dentro da pasta src/database/entities crie os seguintes arquivos

buyer.entity.ts

import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "../../deps.ts";
import Rating from "./rating.entity.ts";

@Entity("buyers")
export default class Buyer {

  @PrimaryGeneratedColumn()
  id!: number;

  @Column({ type: "varchar", length: 256 })
  fullName!: string;

  @OneToMany(type => Rating, _ => _.buyer)
  ratings!: Rating[];
}
Enter fullscreen mode Exit fullscreen mode

product.entity.ts

import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "../../deps.ts";
import Rating from "./rating.entity.ts";

@Entity("products")
export default class Product {

  @PrimaryGeneratedColumn()
  id!: number;

  @Column({ type: "varchar", length: 256 })
  name!: string;

  @OneToMany(type => Rating, x => x.product)
  ratings!: Rating[];
}
Enter fullscreen mode Exit fullscreen mode

rating.entity.ts

import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "../../deps.ts";
import Product from "./product.entity.ts";
import Buyer from "./buyer.entity.ts";

@Entity("ratings")
export default class Rating {

  @PrimaryGeneratedColumn()
  id!: number;

  @Column({ type: "int" })
  rate!: number;

  @Column({ type: "text" })
  comment!: string;

  @ManyToOne(type => Product, _ => _.ratings)
  product!: Product;

  @ManyToOne(type => Buyer, _ => _.ratings)
  buyer!: Buyer;
}
Enter fullscreen mode Exit fullscreen mode

Como pode ver, toda configuração referente ao banco de dados é feita com decorators dentro do TypeORM. Isso é muito legal pois podemos utilizar a mesma classe em outro cenários sem sujá-la com propriedades referente a como os dados estão armazenados.

  • O decorator @Entity() torna nossa classe uma tabela dentro do banco, caso queira você pode dizer o nome que esta tabela terá;
  • @PrimaryGeneratedColumn diz que o campo id é a chave-primária e será auto-incrementável;
  • Com @Column() fazemos as configurações que a coluna terá no banco;
  • Para fazer relacionamento no TypeORM mantemos um campo com o tipo da classe relacionada como referência e usamos o decorator OneToOne(), OneToMany(), ManyToOne() ou ManyToMany() neste campo. Sempre lembrando de fazer tanto a ida quanto a volta para ambas as entidades. Repare que na entidade Rating não criamos a foreign key para Product nem Buyer, o TypeORM faz isso pra gente e nos deixa trabalhar direto com a classe. Muito massa isso 😃

Há diversos outros decorators e opções para os mais diversos cenários dentro do TypeORM. Recomendo olhar a documentação para casos mais complexos.

Connection

Entidades criadas, agora vamos criar a conexão com o banco.

Dentro da pasta src/database crie o arquivo connection.ts com essas configurações:

import { createConnection, Connection } from "../deps.ts";

import Buyer from "./entities/buyer.entity.ts";
import Product from "./entities/product.entity.ts";
import Rating from "./entities/rating.entity.ts";
import env from "../env.ts";

class RatingConnection {

  connection!: Connection;

  async connect() {
    try {
      this.connection = await createConnection({
        type: "postgres",
        host: env.DB_HOST,
        port: env.DB_PORT,
        username: env.DB_USERNAME,
        password: env.DB_PASSWORD,
        database: env.DB_DATABASE,
        entities: [
          Rating,
          Product,
          Buyer,
        ],
        synchronize: true,
        logging: true,
      });

      return this.connection;

    } catch (err) {
      console.error(err);
    }
  }
}

export default new RatingConnection();
Enter fullscreen mode Exit fullscreen mode
  • syncroniz irá deixar o banco igual nossas entidades, criando as tabelas e removendo as que não existem;
  • logger para vermos as queries que a aplicação fará no terminal;

Irei falar sobre postgres e o env.ts logo abaixo.

Repositories

Com TypeORM conseguimos usar um query-builder muito poderoso, podemos fazer queries complexas com código JS/TS. Fazemos essas queries a partir da connection, porém o TypeORM oferece a opção de criar um repository padrão com muitos métodos comuns. A partir da connection chamamos o método getRepository passando o tipo da Entity no parâmetro. Isso irá retornar um novo objeto do tipo Repository<TEntity> que possui métodos como: getId, create, update, delete, find, findOne, entre outros.

Dentro da pasta src/database/repositories vamos criar os seguintes arquivos:

buyer.repository.ts

import { Repository } from '../../deps.ts';
import RatingConnection from '../connection.ts';
import Buyer from '../entities/buyer.entity.ts';

class BuyerRepository {

  private _instance: Repository<Buyer> | undefined;

  get instance() {
    if (!this._instance) {
      this._instance = RatingConnection.connection?.getRepository(Buyer);
    }

    return this._instance;
  }
}

export default new BuyerRepository();
Enter fullscreen mode Exit fullscreen mode

product.repository.ts

import { Repository } from '../../deps.ts';
import Product from '../entities/product.entity.ts';
import RatingConnection from '../connection.ts';

class ProductRepository {

  private _instance: Repository<Product> | undefined;

  get instance() {
    if (!this._instance) {
      this._instance = RatingConnection.connection?.getRepository(Product);
    }

    return this._instance;
  }
}

export default new ProductRepository();
Enter fullscreen mode Exit fullscreen mode

rating.repository.ts

import { Repository } from '../../deps.ts';
import Rating from '../entities/rating.entity.ts';
import RatingConnection from '../connection.ts';

class RatingRepository {

  private _instance: Repository<Rating> | undefined;

  get instance() {
    if (!this._instance) {
      this._instance = RatingConnection.connection?.getRepository(Rating);
    }

    return this._instance;
  }
}

export default new RatingRepository();
Enter fullscreen mode Exit fullscreen mode

Basicamente estamos criando uma classe com um método singleton que irá retornar uma instância do repository.

No TypeORM podemos usar o decorator @EntityRepository(TEntity) para criar esses repositories de maneira automática, porém essa versão apresentou alguns problemas. Uma alternativa seria usar uma lib de dependency injection, mas não vi a necessidade disso pra este projeto.

Controllers

Camada de dados pronta, agora vamos criar os endpoints.

Dentro da pasta src/controllers crie o arquivo rating.controller.ts.

import { Context } from "../deps.ts";
import RatingRepository from '../database/repositories/rating.repository.ts';
import BuyerRepository from '../database/repositories/buyer.repository.ts';
import ProductRepository from '../database/repositories/product.repository.ts';
import Rating from '../database/entities/rating.entity.ts';

class RatingController {

  async index({ response }: Context) {

    const ratings = await RatingRepository.instance
      .createQueryBuilder('ratings')
      .select(['ratings', 'product.name', 'buyer.fullName'])
      .leftJoin('ratings.product', 'product')
      .leftJoin('ratings.buyer', 'buyer')
      .getMany();

    response.body = {
      success: true,
      data: ratings.map((rating: Rating) => {
        return {
          id: rating.id,
          comment: rating.comment,
          rate: rating.rate,
          buyer: rating.buyer.fullName,
          product: rating.product.name
        }
      })
    };
  }

  async create({ response, request }: Context) {
    const body = await request.body();
    const { comment, rate, buyerId, productId } = body.value;

    if (rate < 0 || rate > 5) {
      response.status = 400;
      response.body = {
        success: false,
        message: 'invalid rate'
      }

      return;
    }

    const product = await ProductRepository.instance.findOne(productId)
    const buyer = await BuyerRepository.instance.findOne(buyerId);

    if (!product || !buyer) {
      response.status = 404;
      response.body = {
        success: false,
      }

      return;
    }

    const rating = new Rating();
    rating.buyer = buyer;
    rating.product = product;
    rating.comment = comment;
    rating.rate = rate;
    await RatingRepository.instance.save(rating);

    response.status = 201;
    response.body = {
      success: true,
      data: rating
    }
  }
}

export default new RatingController();
Enter fullscreen mode Exit fullscreen mode

Primeiro importamos os repositories. O método index irá retornar um array de Rating, a partir do RatingRepository fazemos a query dando join nas tabelas relacionadas e select nos campos que iremos usar. O método create irá cadastrar um novo Rating, após pegar os dados do body fazemos uma validação, depois pegamos as entidades relacionadas e, caso não forem nulas, salvamos um novo Rating. Bem simples.

Rotas

Agora dentro de src crie router.ts

import { Router } from './deps.ts'

import RatingController from './controllers/rating.controller.ts';

const router = new Router();

router
  .get('/ratings', RatingController.index)
  .post('/ratings', RatingController.create);

export default router;
Enter fullscreen mode Exit fullscreen mode

Server.ts

Fizemos várias alterações. Agora precisamos voltar ao server.ts e atualizá-lo com as novas classes.

import { Application } from './deps.ts'
import env from './env.ts';
import router from './routes.ts'
import RatingConnection from './database/connection.ts';

await RatingConnection.connect();

const app = new Application();

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

console.log(`Running on port ${env.APP_PORT}`);
await app.listen({ port: env.APP_PORT_INTERNAL || env.APP_PORT });
Enter fullscreen mode Exit fullscreen mode

Deploy

.env

Toda aplicação tem dados sensíveis que mudam de ambiente para ambiente. É prática comum exportar esses valores quando rodamos a aplicação e obtê-los durante o runtime. Para sabermos quais dados devemos passar criamos um arquivo chamado .env.example.

# Application
APP_PORT=
APP_PORT_INTERNAL=
APP_ENVIRONMENT=

# Database
DB_HOST=
DB_PORT=
DB_USERNAME=
DB_PASSWORD=
DB_DATABASE=
Enter fullscreen mode Exit fullscreen mode

Com esse template copiamos todos as variáveis e criamos outro arquivo chamado .env com os devidos valores. Não devemos versionar este último, ele deve existir somente no ambiente na qual a aplicação irá rodar.

Podemos também passar esses valores dentro da chave env do denon.json ou usar alguma lib que lê este tipo de arquivo e o carrega no environment do runtime.

Dockerfile

Docker é uma das ferramentas mais usadas no desenvolvimento de software atualmente. Me atrevo a dizer que saber Docker hoje em dia é quase tão importante quanto saber Git. Ele é muito prático e nos ajuda em várias coisas.

Não existe uma imagem oficial do Deno até o momento, o mais próximo disso é esta imagem que está sendo cogitada nas issues do GitHub.

FROM hayd/deno:alpine-1.0.5

EXPOSE 3333

WORKDIR /app

USER deno

COPY src/deps.ts /app
RUN deno cache --unstable deps.ts

ADD /src /app
RUN deno cache -c tsconfig.json --unstable server.ts

CMD ["run", "--allow-env", "--allow-net", "--config", "tsconfig.json", "--unstable", "server.ts" ]
Enter fullscreen mode Exit fullscreen mode

Cada comando no Docker é como uma camada, onde a anterior se torna a base para as próximas. Com isso em mente:

  • Primeiro obtemos a imagem base com FROM. Estamos usando a versão alpine, que é uma versão mais leve do linux. Estamos usando a versão 1.0.5 do Deno também. É boa prática sempre dizer a versão que sua aplicação usa para não atualizar de uma hora pra outra e, possivelmente, quebrar o sistema;
  • Em seguida expomos a porta 3333 onde podemos receber requisições;
  • WORKDIR é basicamente um $ cd dentro do container. Ele cria a pasta, caso ainda não exista;
  • Mudamos de usuário root com USER;
  • Aqui um ponto importante. Copiamos o arquivo deps.ts e fazemos o cache dele. Este comando só será executado novamente caso as dependências mudem;
  • Depois copiamos o restante da pasta src para dentro do container e cacheamos o server.ts para que não seja compilado toda vez que rodarmos uma nova instância do container.
  • E por último, CMD irá executar os argumentos dentro do array toda vez que o container for iniciado.

Fazendo uma analogia com OOP: O Dockerfile é como uma classe - onde definimos o shape que nosso container terá - e o container é o objeto instanciado.

docker-compose

Para instanciar um container precisamos passar diversos argumentos pelo terminal. Lembrando também que um sistema geralmente não funciona sozinho - no BackEnd, por exemplo, temos a aplicação e o banco de dados. Pra conectar ambos pelo docker precisamos criar uma network. No caso do banco de dados, precisamos passar o volume para manter os dados persistidos. Além disso podemos passar as variáveis de ambiente, expor portas, entre outras coisas.

Para gerenciar tudo isso temos o docker-compose. Sua função é configurar todos os containers que precisamos, com todos os argumentos, e conectá-los.

Iremos criar dois containers

  • app que fará o build da aplicação e configurar seus parâmetros de entrada;
  • database onde iremos configurar um banco de dados da nossa escolha. No meu caso escolhi o postgres, mas pode ser qualquer outro de sua preferência. Só lembre de alterar o type no arquivo connection.ts caso mude de banco;
version: "3.1"

services:
  app:
    build: .
    env_file:
      - .env
    restart: always
    environment: 
      APP_PORT: ${APP_PORT}
      APP_PORT_INTERNAL: 3333
      DB_HOST: ${DB_HOST}
      DB_PORT: 5432
      DB_USERNAME: ${DB_USERNAME}
      DB_PASSWORD: ${DB_PASSWORD}
      DB_DATABASE: ${DB_DATABASE}
    ports:
      - ${APP_PORT}:3333
    depends_on: 
      - database
  database:
    image: postgres
    env_file:
      - .env
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${DB_DATABASE} 
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    ports:
      - ${DB_PORT}:5432
    volumes:
      - "ec.rating.data:/var/lib/postgresql/data"

volumes:
  ec.rating.data:
Enter fullscreen mode Exit fullscreen mode

Em app:

  • Primeiro dizemos em qual diretório o Dockerfile se encontra. Como está no mesmo do docker-compose.yml, utilizamos .;
  • env_file irá ler o .env e carregar os valores pra dentro do container. Usamos a sintaxe ${...} para obter esses valores;
  • restart caso o container pare queremos que ele reinicie sem precisar de intervenção humana;
  • environment passamos para a aplicação todas as variáveis de ambiente que ela precisará;
  • ports mapeamos a porta do nosso host para dentro do container;
  • depends_on para que o app suba após o database. Isso irá evitar o erro do app subir primeiro e tentar conectar com o database que ainda não existe;

Em database:

  • image base do postgres disponível no DockerHub
  • (...) mesma coisa do app
  • Como um container não mantêm estado precisamos mapear um diretório do host para dentro do container. Fazemos isso através dos volumes. Assim, independente de qual container esteja rodando, isso manterá os arquivos salvos já que ele sempre olhará para a mesma pasta.

Quando criamos containers pelo docker-compose eles fazem parte automaticamente da mesma network. Dentro do app, por exemplo, podemos fazer referência ao database pelo seu alias.

A imagem do app não irá compilar a menos que você diga. Antes de mais nada rode $ docker-compose build app para que a imagem seja criada. Feito isso é só rodar $ docker-compose up e deve estar tudo funcionando :).

Testando os endpoints

Eu costumo utilizar o Insomnia para fazer requisições http, mas qualquer um serve.

GET

Utilizando a porta dentro do .env, fazemos uma requisição GET para http://localhost:$PORT/ratings e ele deve retornar um json com esta mesma estrutura:

{
  "success": true,
  "data": [
    {
      "id": 1,
      "comment": "best console",
      "rate": 4,
      "buyer": "Bruce Wayne",
      "product": "PS4"
    },
    {
      "id": 2,
      "comment": "xbox one is better",
      "rate": 2,
      "buyer": "Bruce Wayne",
      "product": "PS4"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

POST

Agora para criar um novo registro envie uma requisição POST para a mesma URL. Com um body nessa estrutura:

{
    "comment": "new rate",
    "rate": 4,
    "productId": 1,
    "buyerId": 1
}
Enter fullscreen mode Exit fullscreen mode

A resposta deve ser algo parecido com isso:

{
  "success": true,
  "data": {
    "buyer": {
      "id": 1,
      "fullName": "Bruce Wayne"
    },
    "product": {
      "id": 1,
      "name": "PS4"
    },
    "comment": "new rate",
    "rate": 4,
    "id": 3
  }
}
Enter fullscreen mode Exit fullscreen mode

AWS

Irei publicar este projeto na AWS, porém como estamos usando Docker, qualquer provedor irá servir e a mudança na publicação é mínima.

Instale o AWS CLI na sua máquina, configure seu acesso pelo terminal e instale o docker-machine. Agora, se foi tudo configurado certo, rodamos $ docker-machine create --driver amazonec2 aws01 e isso irá criar e configurar uma máquina com tudo que precisamos para rodar os containers na AWS. Se rodarmos $ docker-machine env aws01 iremos ver alguns valores referentes a máquina criada. Na última linha terá um comando como este: $ eval $(docker-machine env aws01). Assim que rodá-lo o CLI do Docker irá referenciar esta máquina remota como o Host, desta forma podemos rodar $ docker-compose up -d e isso criará toda a aplicação e o banco de dados neste servidor. Feito isso é só liberar as portas no Dashboard da AWS e pronto.

Para voltar a referenciar sua máquina como host no Docker rode $ eval $(docker-machine -u).

Com Docker conseguimos criar todo um ambiente de maneira automatizada e fazer o Deploy de maneira muito simples. O próximo passo seria criar um processo de CI/CD e fazer este deploy de maneira automática também.

Considerações finais

Vimos neste artigo a criação de uma API com Deno de ponta-a-ponta utilizando práticas de uma aplicação real.

Dado esta experiência posso concluir que Deno é uma promessa para um futuro próximo ainda. Definitivamente não recomendo criar nenhuma aplicação para produção com ele! Mas como bons programadores que somos, é muito legal aprender sobre algo novo, testar e ver tudo funcionando no final. Quem sabe daqui 1 ou 2 anos estaremos com um ambiente estável e discutindo sobre novas features dele. Quem quiser contribuir com a comunidade, este é o melhor momento.

Aprenda, teste e ensine.
DEVELOPERS TOGETHER STRONG

Top comments (5)

Collapse
 
davidalencarignacio profile image
DAVID ALENCAR

Ótimo artigo =)

Collapse
 
guisfits profile image
Guilherme Camargo

Valeu mann! tmj 👊

Collapse
 
rubensdemelo profile image
rubensdemelo

Parabens 👏👏👏

Collapse
 
guisfits profile image
Guilherme Camargo

Obrigado Rubens!!!

Collapse
 
guisfits profile image
Guilherme Camargo

I intend to translate some of my articles to english soon, keep watch.
Dinosaurs show is so nostalgic, "Not The Momma" lol