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:
- Latest TypeScript and Javascript Grammar – Extensão da Microsoft para suporte de Typescript e Javascript
- Typescript Hero – Organiza os imports do typescript
- ESLint – Integração do ESLint diretamente na IDE
- Prettier – Code Formatter – Integração do Prettier diretamente na IDE
- Docker – Para autocomplete, destaque de código e comandos do Docker
- Material Icon Theme – Esse não é necessário, mas eu gosto dos ícones bonitinhos e quis compartilhar
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,
"[javascript]": {
"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:
- Definimos a variável de ambiente
NODE_ENV
paraproduction
, isso evitará que as dependências listadas em devDependencies em nossopackage.json
sejam instaladas. - 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!
Ótimo artigo Henrique! Ajudando de mais!
Fiquei apenas com uma dúvida, onde você realiza o build do Typescript para enviar para produção? Na sua pipeline de CI?
Abraço
Fala Victor, que bom que gostou!
Então, tem um erro ai no artigo, na imagem pra produção tem um step chamado “RUN npm ci” que deveria ser “RUN npm run build”, e ai esse script build seria o tsc. Essa etapa de build do typescript fica no próprio dockerfile de produção.
Vou corrigir aqui hehehe abraços!
Olá Henrique. Estou tentando fazer o docker-compose up e estou recebendo o warning:
Experimental support for decorators is a feature that is subject to change in a future release. Set the ‘experimentalDecorators’ option in your ‘tsconfig’ or ‘jsconfig’ to remove this warning.
Mesmo tendo definido essa variável dentro do tsconfig.json. Saberia como resolver isso? Configurei também o VS Code e nada.
Obrigado de antemão pela ajuda e parabéns pelo post.
Fala Fabiano, você colocou as duas flags?
E mesmo assim não foi? Tentou reiniciar o VSCode, ele da umas bugada as vezes quando mudamos o tsconfig e precisamos recarregar ele.
Isso mesmo Henrique. Coloquei ambas as flags, reiniciei o VS Code e mesmo assim não foi. Cara, estou pesquisando a resolução desse problema tem algumas semanas. Estou com um projeto pessoal em TypeScript e não consigo colocar ele no ar, pra rodar na Cloud. Justamente por conta desse erro que acabei chegando no seu post. Se você quiser e puder me ajudar, gostaria de poder trocar mais ideias contigo sobre esse caso. Não consigo de forma alguma fazer ele rodar na Google Cloud. E pelo erro é como se estivesse faltando alguma etapa de “configuração” no Dockerfile. Quando coloco apenas… Read more »
Tranquilo! Me manda um email henrique@marquesfernandes.com e a gente tenta resolver junto