O que Todo Dev Deve Saber sobre Node.js com TypeScript: Configuração, tsx e ts-node na Prática Já leu

Node.js com TypeScript: Configuração, tsx e ts-node na Prática TypeScript é uma linguagem que adiciona tipagem estática ao JavaScript, permitindo capturar erros em tempo de desenvolvimento e melhorar a qualidade do código. Quando trabalhamos com Node.js, precisamos de ferramentas que executem nossos arquivos TypeScript diretamente, sem precisar compilar manualmente para JavaScript antes de cada execução. Este artigo vai te ensinar a configurar um projeto Node.js com TypeScript do zero, usando as ferramentas mais modernas e práticas do mercado. Entendendo o Cenário Por que TypeScript no Node.js? Node.js executa JavaScript nativamente, mas TypeScript precisa ser transpilado (convertido) para JavaScript antes de ser executado. Sem as ferramentas certas, esse processo se torna tedioso e improdutivo. Existem basicamente três abordagens para resolver isso: Compilar manualmente: usar (TypeScript Compiler) para gerar arquivos , depois executar com . Executar diretamente com ts-node: carrega e executa arquivos TypeScript na memória sem gerar arquivos intermediários. Usar tsx (moderno): similar ao ts-node, mas mais rápido, com melhor suporte

Node.js com TypeScript: Configuração, tsx e ts-node na Prática

TypeScript é uma linguagem que adiciona tipagem estática ao JavaScript, permitindo capturar erros em tempo de desenvolvimento e melhorar a qualidade do código. Quando trabalhamos com Node.js, precisamos de ferramentas que executem nossos arquivos TypeScript diretamente, sem precisar compilar manualmente para JavaScript antes de cada execução. Este artigo vai te ensinar a configurar um projeto Node.js com TypeScript do zero, usando as ferramentas mais modernas e práticas do mercado.

Entendendo o Cenário

Por que TypeScript no Node.js?

Node.js executa JavaScript nativamente, mas TypeScript precisa ser transpilado (convertido) para JavaScript antes de ser executado. Sem as ferramentas certas, esse processo se torna tedioso e improdutivo. Existem basicamente três abordagens para resolver isso:

  1. Compilar manualmente: usar tsc (TypeScript Compiler) para gerar arquivos .js, depois executar com node.
  2. Executar diretamente com ts-node: carrega e executa arquivos TypeScript na memória sem gerar arquivos intermediários.
  3. Usar tsx (moderno): similar ao ts-node, mas mais rápido, com melhor suporte a ECMAScript modules e sem necessidade de configurações complexas.

A escolha certa impacta na velocidade do desenvolvimento e na experiência durante execução de scripts, testes e aplicações em produção.

Configuração Inicial do Projeto

Criando o Projeto e Instalando Dependências

Vamos começar do zero. Crie uma pasta para seu projeto e inicialize o Node.js:

mkdir meu-projeto-typescript
cd meu-projeto-typescript
npm init -y

Agora, instale as dependências necessárias. Você vai precisar do TypeScript, do ts-node para desenvolvimento, e do tsx como alternativa moderna:

npm install --save-dev typescript ts-node @types/node tsx

O pacote @types/node fornece as definições de tipos para as APIs do Node.js, permitindo que você tenha autocompletar e verificação de tipos ao trabalhar com módulos nativos como fs, http, path, etc.

Configurando o tsconfig.json

O arquivo tsconfig.json é o coração da configuração TypeScript. Ele define como o compilador deve processar seus arquivos. Crie esse arquivo na raiz do projeto:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Alguns pontos importantes aqui:

  • target: ES2020 significa que seu código será compilado para JavaScript moderno. Se precisa suportar ambientes muito antigos, reduza esse valor.
  • module: commonjs é o padrão do Node.js. Use esnext se preferir ES modules.
  • strict: true ativa todas as verificações de tipo, forçando você a ser mais cuidadoso com o código.
  • outDir e rootDir: definem onde estão seus arquivos TypeScript (src) e onde irão os compilados (dist).
  • esModuleInterop: permite usar imports comuns como import express from 'express' em vez de import * as express.

Crie a pasta src onde seus arquivos TypeScript ficarão:

mkdir src

Usando ts-node para Desenvolvimento

O que é ts-node?

ts-node é um executor TypeScript para Node.js que compila e executa arquivos TypeScript em tempo real, sem gerar arquivos .js intermediários. É perfeito para desenvolvimento, testes rápidos e scripts ocasionais. Você já o instalou no passo anterior.

Executando Arquivos com ts-node

Crie um arquivo simples para testar. Abra src/index.ts:

interface Usuario {
  id: number;
  nome: string;
  email: string;
  ativo: boolean;
}

function criarUsuario(id: number, nome: string, email: string): Usuario {
  return {
    id,
    nome,
    email,
    ativo: true
  };
}

const usuario = criarUsuario(1, "João Silva", "joao@example.com");
console.log("Usuário criado:", usuario);
console.log(`Email: ${usuario.email}, Ativo: ${usuario.ativo}`);

Agora execute usando ts-node:

npx ts-node src/index.ts

Você verá:

Usuário criado: { id: 1, nome: 'João Silva', email: 'joao@example.com', ativo: true }
Email: joao@example.com, Ativo: true

A vantagem aqui é que TypeScript validou seus tipos em tempo de execução. Se você tentar passar um valor inválido, o ts-node vai reclamar antes de rodar o código.

Configurando Scripts no package.json

Para facilitar a execução, adicione scripts ao seu package.json:

{
  "scripts": {
    "dev": "ts-node src/index.ts",
    "start": "node dist/index.js",
    "build": "tsc",
    "build:watch": "tsc --watch"
  }
}

Agora você pode rodar npm run dev em vez de digitar o comando completo. O script build compila seu TypeScript para JavaScript em dist/, útil para produção.

Limitações do ts-node

Apesar de prático, ts-node tem algumas limitações. Ele é mais lento que executar JavaScript puro, pois faz a compilação a cada execução. Para aplicações com muitos arquivos, esse overhead fica visível. Além disso, em alguns casos com ES modules (imports modernos), pode haver comportamentos inesperados.

Usando tsx: A Alternativa Moderna

O que é tsx?

tsx é um executor TypeScript moderno, mais rápido que ts-node e com melhor suporte a ES modules. É mantido pelo criador do esbuild e traz performance superior. Você já instalou no projeto anterior.

Executando com tsx

A sintaxe é praticamente idêntica ao ts-node:

npx tsx src/index.ts

Você receberá o mesmo resultado, mas a execução será mais rápida. Para aplicações maiores, essa diferença fica evidente.

Migrando de ts-node para tsx

Se já tem scripts usando ts-node, é trivial migrar. Crie um exemplo com lógica um pouco mais complexa. Crie src/api-simulada.ts:

interface Produto {
  id: number;
  nome: string;
  preco: number;
  estoque: number;
}

interface ResultadoBusca {
  produtos: Produto[];
  total: number;
  tempo_ms: number;
}

async function buscarProdutos(termo: string): Promise<ResultadoBusca> {
  const inicio = Date.now();

  // Simula uma busca em banco de dados
  const produtosMock: Produto[] = [
    { id: 1, nome: "Notebook Dell", preco: 3500, estoque: 5 },
    { id: 2, nome: "Mouse Logitech", preco: 150, estoque: 20 },
    { id: 3, nome: "Teclado Mecânico", preco: 450, estoque: 10 },
  ];

  // Filtra por termo (simulado)
  const produtosFiltrados = produtosMock.filter(p =>
    p.nome.toLowerCase().includes(termo.toLowerCase())
  );

  const tempo = Date.now() - inicio;

  return {
    produtos: produtosFiltrados,
    total: produtosFiltrados.length,
    tempo_ms: tempo
  };
}

// Executar a busca
(async () => {
  const resultado = await buscarProdutos("notebook");
  console.log("Resultado da busca:", resultado);
})();

Execute com tsx:

npx tsx src/api-simulada.ts

Saída esperada:

Resultado da busca: {
  produtos: [
    { id: 1, nome: 'Notebook Dell', preco: 3500, estoque: 5 }
  ],
  total: 1,
  tempo_ms: 2
}

Configurando tsx no package.json

Atualize seus scripts para usar tsx:

{
  "scripts": {
    "dev": "tsx src/index.ts",
    "dev:watch": "tsx watch src/index.ts",
    "start": "node dist/index.js",
    "build": "tsc",
    "build:watch": "tsc --watch"
  }
}

A flag watch do tsx monitora mudanças no arquivo e o reexecuta automaticamente, ótimo para desenvolvimento iterativo.

Quando Usar tsx vs ts-node

  • tsx: Para a maioria dos casos modernos. É mais rápido, tem melhor suporte a ES modules, e requer menos configuração.
  • ts-node: Para projetos legados com CommonJS pesado, ou quando você precisa de um suporte muito específico à sua stack.

Compilando para Produção

O Fluxo Desenvolvimento vs Produção

Durante desenvolvimento, você executa TypeScript diretamente com tsx ou ts-node. Mas em produção, você não quer instalar ts-node ou tsx (isso aumentaria o tamanho das dependências). A solução é compilar TypeScript para JavaScript antes de fazer deploy.

Compilando com tsc

Execute o comando de build:

npm run build

Isso executa tsc, que lê seu tsconfig.json e compila todos os arquivos .ts em src/ gerando .js em dist/. Verifique:

ls -la dist/

Você verá arquivos como index.js, api-simulada.js, etc. Abra um deles para ver como ficou o JavaScript gerado:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function criarUsuario(id, nome, email) {
    return {
        id,
        nome,
        email,
        ativo: true
    };
}
const usuario = criarUsuario(1, "João Silva", "joao@example.com");
console.log("Usuário criado:", usuario);

Repare que os tipos foram removidos (TypeScript não precisa deles em runtime), mas a lógica permanece idêntica.

Executando em Produção

Em produção, você roda o JavaScript compilado:

npm run build
npm start

Isso executa node dist/index.js — puro JavaScript, sem overhead de compilação.

Otimizações para Produção

Para projetos maiores, considere:

  1. Minificação: use ferramentas como esbuild ou webpack para reduzir o tamanho dos arquivos.
  2. Tree-shaking: remover código não utilizado.
  3. Source maps: gerar arquivos .map para facilitar debugging em produção (você já tem sourceMap: true no tsconfig.json).

Exemplo com esbuild:

npm install --save-dev esbuild

Crie um script de build otimizado em scripts/build.js:

const esbuild = require('esbuild');

esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  minify: true,
  outfile: 'dist/index.min.js',
  target: 'node16',
  sourcemap: true,
  platform: 'node'
}).catch(() => process.exit(1));

Execute com node scripts/build.js para gerar um arquivo minificado e otimizado.

Exemplo Prático Completo: Uma Aplicação Express

Para solidificar o aprendizado, vamos criar uma pequena API REST com Express. Instale o Express e seus tipos:

npm install express
npm install --save-dev @types/express

Crie src/server.ts:

import express, { Request, Response } from 'express';

interface Task {
  id: number;
  titulo: string;
  concluida: boolean;
  dataCriacao: Date;
}

const app = express();
const PORT = 3000;

// Middleware
app.use(express.json());

// Armazenamento em memória (em produção, use um banco de dados)
let tarefas: Task[] = [
  {
    id: 1,
    titulo: 'Aprender TypeScript',
    concluida: false,
    dataCriacao: new Date('2024-01-15')
  },
  {
    id: 2,
    titulo: 'Dominar Node.js',
    concluida: false,
    dataCriacao: new Date('2024-01-20')
  }
];

let proximoId = 3;

// Rota GET - listar todas as tarefas
app.get('/tarefas', (req: Request, res: Response) => {
  res.json({
    sucesso: true,
    dados: tarefas,
    total: tarefas.length
  });
});

// Rota GET - obter uma tarefa por ID
app.get('/tarefas/:id', (req: Request, res: Response) => {
  const { id } = req.params;
  const tarefa = tarefas.find(t => t.id === parseInt(id));

  if (!tarefa) {
    res.status(404).json({
      sucesso: false,
      mensagem: 'Tarefa não encontrada'
    });
    return;
  }

  res.json({
    sucesso: true,
    dados: tarefa
  });
});

// Rota POST - criar nova tarefa
app.post('/tarefas', (req: Request, res: Response) => {
  const { titulo } = req.body;

  if (!titulo || typeof titulo !== 'string') {
    res.status(400).json({
      sucesso: false,
      mensagem: 'Título é obrigatório e deve ser uma string'
    });
    return;
  }

  const novaTarefa: Task = {
    id: proximoId++,
    titulo,
    concluida: false,
    dataCriacao: new Date()
  };

  tarefas.push(novaTarefa);

  res.status(201).json({
    sucesso: true,
    mensagem: 'Tarefa criada com sucesso',
    dados: novaTarefa
  });
});

// Rota PUT - atualizar tarefa
app.put('/tarefas/:id', (req: Request, res: Response) => {
  const { id } = req.params;
  const { titulo, concluida } = req.body;

  const tarefa = tarefas.find(t => t.id === parseInt(id));

  if (!tarefa) {
    res.status(404).json({
      sucesso: false,
      mensagem: 'Tarefa não encontrada'
    });
    return;
  }

  if (titulo) tarefa.titulo = titulo;
  if (typeof concluida === 'boolean') tarefa.concluida = concluida;

  res.json({
    sucesso: true,
    mensagem: 'Tarefa atualizada com sucesso',
    dados: tarefa
  });
});

// Rota DELETE - deletar tarefa
app.delete('/tarefas/:id', (req: Request, res: Response) => {
  const { id } = req.params;
  const indice = tarefas.findIndex(t => t.id === parseInt(id));

  if (indice === -1) {
    res.status(404).json({
      sucesso: false,
      mensagem: 'Tarefa não encontrada'
    });
    return;
  }

  const [tarefaDeletada] = tarefas.splice(indice, 1);

  res.json({
    sucesso: true,
    mensagem: 'Tarefa deletada com sucesso',
    dados: tarefaDeletada
  });
});

// Rota de health check
app.get('/health', (req: Request, res: Response) => {
  res.json({
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});

// Iniciar servidor
app.listen(PORT, () => {
  console.log(`✓ Servidor rodando em http://localhost:${PORT}`);
  console.log(`✓ Health check: http://localhost:${PORT}/health`);
  console.log(`✓ Listar tarefas: GET http://localhost:${PORT}/tarefas`);
});

Atualize o script de desenvolvimento no package.json:

{
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "start": "node dist/server.js",
    "build": "tsc"
  }
}

Execute a aplicação:

npm run dev

Saída esperada:

✓ Servidor rodando em http://localhost:3000
✓ Health check: http://localhost:3000/health
✓ Listar tarefas: GET http://localhost:3000/tarefas

Teste as rotas com curl ou Insomnia:

# Listar tarefas
curl http://localhost:3000/tarefas

# Criar nova tarefa
curl -X POST http://localhost:3000/tarefas \
  -H "Content-Type: application/json" \
  -d '{"titulo":"Estudar TypeScript avançado"}'

# Obter tarefa por ID
curl http://localhost:3000/tarefas/1

# Atualizar tarefa
curl -X PUT http://localhost:3000/tarefas/1 \
  -H "Content-Type: application/json" \
  -d '{"concluida":true}'

# Deletar tarefa
curl -X DELETE http://localhost:3000/tarefas/1

A segurança de tipos do TypeScript trabalha aqui em múltiplas frentes: validação de tipos nos handlers, tipos explícitos para Request e Response, e verificação de tipos quando você acessa as tarefas.

Conclusão

Você aprendeu os três pilares essenciais para trabalhar com TypeScript em Node.js. Primeiro, compreender que TypeScript precisa de transpilação, e que existem ferramentas modernas para fazer isso de forma transparente durante desenvolvimento. Segundo, tsx é a escolha recomendada para projetos novos: é mais rápido, requer menos configuração que ts-node, e tem melhor suporte a ES modules — use-o para desenvolvimento e scripts. Terceiro, sempre compile seu TypeScript com tsc antes de fazer deploy em produção, removendo a dependência de executores de runtime e reduzindo o tamanho das dependências.

Referências


Artigos relacionados