Esse artigo foi inspirado pela minha própria frustração em otimizar o meu setup NodeJS com Typescript e Docker. A maioria dos processos e tutoriais levam a configurações que tornam o desenvolvimento cansativo e lento, entre tantas recompilações e reinicializações sua paciência acaba e sua produtividade vai por água abaixo. Após muita pesquisa, testes e estresse, consegui montar uma configuração ideal!

É necessário que você tenha pelo menos o conhecimento básico de node, typescript e docker, não explicarei nenhuma tecnologia a fundo, se tiverem alguma dúvida específica ficarei feliz em ajudar nos comentários.

Ao final desse tutorial você terá um ambiente de desenvolvimento NodeJS com Typescript, ts-node-dev, Docker, ESlint com Airbnb Style Guide e Prettier.

Todos os códigos desse tutorial estão disponíveis no GitHub.

Na primeira parte do artigo vamos configurar a nossa IDE Visual Studio Code para o desenvolvimento, fique a vontade para pular essa parte caso você utilize outra IDE.

Configurando o VS Code

Primeiro vamos criar uma pasta vazia para o nosso projeto e iniciar o VS Code nela:

$ mkdir node-ts-otimizado && code node-ts-otimizado/

Extensões úteis do VS Code

Recomendo a instalação das extensões listadas abaixo, elas vão dar um boost na sua produtividade:

Configurando o Workspace

Dentro do seu projeto, caso ainda não exista, crie uma pasta .vscode e nela o arquivo settings.json. Adicione as seguintes propriedades:

{
  "eslint.autoFixOnSave":  true,
  "eslint.validate":  [
    "javascript",
    {"language":  "typescript",  "autoFix":  true  },
  ],
  "editor.formatOnSave":  true,
  "":  {
    "editor.formatOnSave":  false,
  },
  "[typescript]":  {
    "editor.formatOnSave":  false,
  }
}

Isso habilita automaticamente o auto-corretor do ESlint e Prettier ao se salvar algum arquivo.

Inicializando um projeto NodeJS

Agora precisamos inicializar um projeto node:

$ cd node-ts-otimizado && npm init

Dentro do projeto vamos criar uma pasta src/, é nela que vamos colocar todos os nossos arquivos fontes .ts. Aproveite e crie um arquivo vazio com o nome index.ts, usaremos ele mais tarde.

TypeScript e ts-node-dev

Precisamos agora instalar todas as dependências que vamos precisar para o nosso ambiente de desenvolvimento:

$ npm i --save-dev typescript ts-node-dev 

A opção –save-dev instala as dependências como devDependencies, porque elas não serão necessárias e nem instaladas em nossa imagem Docker de produção.

  • typescript: Lib oficial para compilar os nossos arquivos .ts
  • ts-node-dev: Habilita o REPL para TypeScript, com reinicialização automática, o que permite nosso código TypeScript funcionar em tempo real, sem compilação (pense em nodemon ou node-dev, mas para TypeScript).

Crie o arquivo tsconfig.json com as configurações para o compilador do Typescript:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "build"
  }
}

Em target vamos utilizar as versão 2020 do ECMAScript, você pode alterar a versão conforme as necessidades do seu projeto.

ESLint e Prettier

Resolvi escolher o ESLint como o linter para esse setup pelo simples motivo que houve o anúncio de descontinuação do projeto TSLint, embora tenha usado ele e gostado em outros projetos, não vale a pena investir em uma dependência importante, que já tem seus dias de vida contados. Instale localmente o ESLint e todas suas dependências:

$ npm i --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-airbnb-base eslint-plugin-import eslint-config-prettier eslint-plugin-prettier prettier

Na raiz do seu projeto crie um arquivo .eslintrc.js de configuração do ESLint:

module.exports = {
    parser: '@typescript-eslint/parser',
    parserOptions: {
      sourceType: 'module',
      project: './tsconfig.json',
    },
    extends:  [
      'airbnb-base', // Adicionaas regras do Airbnb Style Guide
      'plugin:@typescript-eslint/recommended',  // Adiciona as recomendações padrões @typescript-eslint/eslint-plugin
      'prettier/@typescript-eslint',  // Adiciona as configurações do prettier para evitar conflitos de regras @typescript-eslint/eslint-plugin
      'plugin:prettier/recommended',  // Adiciona o plugin do prettier
    ],
  }

Agora crie o arquivo .prettierrc.js de configuração do Prettier:

module.exports = {
  semi: true,
  trailingComma: 'all',
  singleQuote: false,
  printWidth: 120,
  tabWidth: 2,
};

Agora vamos adicionar um script em nosso arquivo package.json para executar o lint:

...
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "lint": "eslint --fix ./src/*"
}
...

Esse comando faz basicamente com que o ESLint analise todos os arquivos dentro da pastasrc/e tente arrumar automaticamente qualquer problema possível. Nem todos os problemas são corrigidos automaticamente, e para ser sincero a grande maioria dos problemas importantes você precisará arrumar manualmente.

Execute npm run lint e verifique que nenhum erro deve ser retornado.

Se você estiver usando o VS Code com a configuração do início do artigo, esses erros vão aparecer ressaltados automaticamente na sua IDE e ao salvar algum arquivo o ESLint tentará corrigir qualquer problema e o Prettier fará a formatação automática.

Desenvolvendo em Typescript sem compilar o tempo todo

Se você já desenvolveu com Typescript provavelmente já se irritou com todo o processo de compilação e reinicialização da sua aplicação. Existem diversas maneiras de configurar seu ambiente para compilar seus arquivos .ts e reinicializar sua aplicação, aqui vamos focar no setup que eu senti mais produtivo, usando a lib ts-node-dev. Essa biblioteca compila o Typescript mas compartilha essa compilação entre a reinicialização da aplicação, isso significa que vamos conseguir ter um auto-reload sem precisar esperar todo o processo de compilação. A lib ts-node-dev é uma mistura de outras duas bibliotecas, node-dev com ts-node.

Vamos criar o script dev que será utilizado durante o desenvolvimento:

...
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "lint": "eslint --fix ./src/*",
  "dev": "ts-node-dev --inspect=8181 --respawn --transpileOnly src/index.ts"
}
...
  • --inspect Define a porta em que o debugger estará escutando.
  • --respawn Continua observando os arquivos por mudanças mesmo se o processo principal morrer.
  • --transpileOnly Desabilita a checagem de tipagem e o saída dos arquivos de definições, promovendo uma transpilação mais rápida.

Adicionando algum código real no projeto

Vamos adicionar um código simples para conseguir testar nossa configuração. Instale a dependência express e sua tipagem:

$ npm i --save express
$ npm install --save-dev @types/express @types/node

Agora abra o arquivo index.ts e cole o seguinte código:

import * as express from "express";

const PORT = 8080; // Porta do nosso servidor web

const app = express(); // Criamos uma instância do express

// Adicionamos uma rota de teste
app.get("/hello-world", (req: express.Request, res: express.Response) => {
  res.json({
    message: "Hello World",
  });
});

// Iniciamos o nosso servidor web
app.listen(PORT, () => {
  console.log(`Aplicação escutando na porta ${PORT}`);
});

Rode o comando npm run dev , abra seu navegador e acesse http://localhost:8080/hello-world

Testando nossa nova configuração

Para testar se a nossa configuração foi bem sucedida, vamos modificar o nosso código original e adicionar uma nova rota:

import * as express from "express";

const PORT = 8080; // Porta do nosso servidor web

const app = express(); // Criamos uma instância do express

// Adicionamos uma rota de teste
app.get("/hello-world", (req: express.Request, res: express.Response) => {
  res.json({
    message: "Hello World",
  });
});

// Adicionamos uma rota de teste com parametros
app.get("/hello-world/:nome", (req: express.Request, res: express.Response) => {
  const { nome } = req.params;
  res.json({
    message: `Olá ${nome}!`,
  });
});

// Iniciamos nosso servidor web
app.listen(PORT, () => {
  console.log(`Aplicação escutando na porta ${PORT}`);
});

Salve o arquivo e veja a mágica acontecer, o resultado esperado é que a aplicação identifique a nossa modificação e atualize o processo automaticamente. Para validar acesse http://localhost:8080/helo-world/henrique:

Dockerizando a aplicação

Vamos criar o arquivo Dockerfile.dev que será a configuração da nossa imagem de desenvolvimento:

FROM node:12-alpine

WORKDIR /app

ADD package*.json ./

RUN npm i

Agora precisamos criar o arquivo docker-compose.yml:

version: "3.7"

services:
  node-ts-otimizado:
    build:
      context: .
      dockerfile: Dockerfile.dev
    container_name: example-web-server
    volumes:
      - ./src:/app/src
    ports:
      - "8080:8080"
      - "8181:8181"
    command: npm run dev

Vamos testar nosso desenvolvimento iniciando o docker compose:

$ docker-compose up

Repita os passos da última etapa e altere alguns códigos, verifique em seu navegador se sua aplicação inicializou e se seu código está atualizando.

Configurando o debugger no VS Code

Como estamos desenvolvendo dentro do nosso container, precisamos acessar o debug remoto do node, por isso liberamos a porta 8181 no docker compose e também em nosso script dev do package.json. Vamos criar um arquivo launch.json dentro da nossa pasta .vscode e colar a configuração:

{
  "type": "node",
  "request": "attach",
  "name": "Docker ts-node",
  "address": "localhost",
  "port": 8181,
  "localRoot": "${workspaceFolder}",
  "remoteRoot": "/app",
  "protocol": "inspector"
}

Agora já podemos inicializar o debugger. Se você estiver no VS Code, pressione F5.

Criando a imagem Docker para produção

Finalmente vamos criar o script da imagem que será implementar em produção, ela possui algumas diferenças de otimização:

FROM node:12-alpine

WORKDIR /home/node/app

ADD . .

ENV NODE_ENV=production

RUN npm ci

USER node

EXPOSE 8080

CMD [ "node", "build/index.js" ]

As diferenças do arquivo Dockerfile.dev para o Dockerfile são:

  1. Definimos a variável de ambiente NODE_ENV para production, isso evitará que as dependências listadas em devDependencies em nosso package.json sejam instaladas.
  2. Por boas práticas não vamos utilizar os “alias” de script do npm para iniciar nossa aplicação, isso reduz o número de processos iniciados e obriga que os sinais de finalização SIGTERM e SIGINT sejam recebidos diretamente pelo processo Node ao invés de ser interceptados pelo npm: Docker Node – Boas Práticas.

Conclusão

Aprendemos como configurar um ambiente de desenvolvimento para NodeJS com Typescript, com auto-reload e linter. Se você tem alguma dica para aprimorar essa configuração, por favor deixe seu comentário!

0 0 voto
Nota do Artigo
Author

Um nerd nada tradicional… Desenvolvedor web full-stack, escritor amador e inventor nas horas vagas. Apaixonado por tecnologia e entusiasmado por projetos de código aberto!

Subscribe
Notify of
guest
0 Comentários
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x