Como configurar uma API Node.js com TypeScript em 2026
Se você aprendeu a montar API em Node com TypeScript há um ou dois anos, metade do que você configurou não é mais necessária. O ts-node, o nodemon, o passo de build antes de rodar — o Node faz tudo isso sozinho agora.
Este guia monta uma API do zero com o que existe hoje, e explica o que você pode remover do seu setup atual. Todos os comandos e saídas abaixo foram executados num container limpo em 15/08/2026.
Versões usadas: Node v24.19.0 (LTS Krypton) · npm 11.17.0 · Express 5.2.1 · TypeScript 7.0.2 · @types/node 26.2.0
O que mudou desde 2024
Três coisas, e todas apagam configuração:
| antes | agora |
|---|---|
ts-node ou tsx para rodar .ts |
node arquivo.ts direto |
nodemon para recarregar |
node --watch nativo |
express-async-errors para erro em rota async |
Express 5 trata sozinho |
Nenhuma dessas dependências precisa existir num projeto novo.
O Node roda TypeScript sozinho
Crie um arquivo .ts e rode:
$ cat t.ts
type Pedido = { id: number; total: number };
const p: Pedido = { id: 1, total: 99.9 };
console.log("rodou:", p.id, p.total);
$ node t.ts
rodou: 1 99.9
Sem instalar nada. Sem flag. O Node 24 lê o arquivo, apaga as anotações de tipo e executa o JavaScript que sobra.
A partir de qual versão isso funciona
"Node 22" não é resposta suficiente: o suporte entrou no meio da série. Testei quatro versões:
| versão | node arquivo.ts sem flag |
|---|---|
| v22.5.1 | ✗ — a flag --experimental-strip-types nem existe |
| v22.6.0 | ✗ — só funciona com a flag |
| v22.17.1 | ✗ — ainda precisa da flag |
| v22.18.0 | ✓ funciona |
| v24.19.0 (LTS) | ✓ funciona |
Ou seja: 22.18 é a fronteira. Se a sua máquina tem um 22 mais antigo, ou você atualiza, ou continua com a flag.
⚠️ Mas ele não checa os tipos
Esta é a parte que quase todo tutorial esquece, e ela muda como você trabalha. O Node apaga os tipos — ele não os verifica. Veja:
$ cat erro.ts
const n: number = "isso e string";
console.log("rodou mesmo com erro de tipo:", n);
$ node erro.ts
rodou mesmo com erro de tipo: isso e string
Um erro de tipo escancarado, e o programa roda. O mesmo arquivo, no compilador:
$ npx tsc --noEmit erro.ts
erro.ts(1,7): error TS2322: Type 'string' is not assignable to type 'number'.
A conclusão prática: o node arquivo.ts substitui o ts-node para executar, mas não substitui o tsc para verificar. Você continua precisando do TypeScript instalado e de um passo de checagem — no editor, no pre-commit ou no CI.
O que o Node não consegue apagar
Só sintaxe apagável funciona. enum, namespace e decorators geram código de verdade, então não passam:
$ node en.ts
enum Status { Novo, Pago }
^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError
Dá para ligar a transformação completa:
$ node --experimental-transform-types en.ts
1
Mas a saída melhor é não usar essa sintaxe. Coloque erasableSyntaxOnly no tsconfig e o compilador te avisa antes de virar erro em produção:
$ npx tsc --noEmit --erasableSyntaxOnly en.ts
en.ts(1,6): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
Em vez de enum, use um objeto as const — é apagável e dá o mesmo resultado.
Montando a API
1. O projeto
mkdir minha-api && cd minha-api
npm init -y
npm install express
npm install -D typescript @types/node @types/express
Repare no que não foi instalado: nada de ts-node, tsx ou nodemon.
2. O passo que quebra todo mundo
Abra o package.json e acrescente "type": "module". Sem isso, a primeira linha de import derruba tudo:
Warning: Failed to load the ES module: /app/src/server.ts.
Make sure to set "type": "module" in the nearest package.json
import express from "express";
^^^^^^
SyntaxError: Cannot use import statement outside a module
Esse erro é o mais comum na configuração e a mensagem não é óbvia: parece problema de TypeScript, mas é do sistema de módulos.
3. O tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src"]
}
Duas opções merecem explicação:
erasableSyntaxOnly— impede que você escreva a sintaxe que o Node não apaga. É o que evita descobrir o problema só no deploy.noEmit: true— o TypeScript aqui não gera arquivo, só confere. Quem executa é o Node. Se você vai empacotar para produção, aí sim troca por um build.
4. A API
import express, { type Request, type Response, type NextFunction } from "express";
type Pedido = { id: number; cliente: string; total: number };
const pedidos: Pedido[] = [{ id: 1, cliente: "Ana", total: 249.9 }];
const app = express();
app.use(express.json());
app.get("/pedidos/:id", (req: Request, res: Response) => {
const id = Number(req.params.id);
const pedido = pedidos.find((p) => p.id === id);
if (!pedido) return res.status(404).json({ erro: "pedido nao encontrado" });
res.json(pedido);
});
app.post("/pedidos", (req: Request, res: Response) => {
const { cliente, total } = req.body as Partial<Pedido>;
if (!cliente || typeof total !== "number") {
return res.status(400).json({ erro: "cliente e total sao obrigatorios" });
}
const novo: Pedido = { id: pedidos.length + 1, cliente, total };
pedidos.push(novo);
res.status(201).json(novo);
});
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
res.status(500).json({ erro: err.message });
});
app.listen(3000, () => console.log("no ar em 3000"));
O type antes de Request não é enfeite: com verbatimModuleSyntax ligado, importar tipo sem essa palavra vira erro. É o que impede o tipo de sobrar no JavaScript final.
5. Rodando
$ node src/server.ts
no ar em 3000
E funcionando:
$ curl localhost:3000/pedidos/1
{"id":1,"cliente":"Ana","total":249.9}
$ curl -X POST localhost:3000/pedidos \
-H "Content-Type: application/json" \
-d '{"cliente":"Bruno","total":80}'
{"id":2,"cliente":"Bruno","total":80}
$ curl -i localhost:3000/pedidos/99
HTTP/1.1 404 Not Found
Durante o desenvolvimento, acrescente --watch e o Node reinicia sozinho a cada salvamento:
node --watch src/server.ts
Express 5: o erro em rota async agora funciona
No Express 4, um throw dentro de handler async não chegava no middleware de erro: a promise rejeitava sozinha e a requisição ficava pendurada. Por isso existia o express-async-errors.
No Express 5 isso é nativo. Testei com esta rota:
app.get("/quebra", async (_req, _res) => {
throw new Error("estourei dentro de um async");
});
O resultado:
$ curl -i localhost:3000/quebra
HTTP/1.1 500 Internal Server Error
$ curl localhost:3000/pedidos/1
{"id":1,"cliente":"Ana","total":249.9}
O erro virou 500 pelo middleware, e o servidor continuou de pé. Se o seu projeto tem express-async-errors no package.json, ele virou dependência morta.
⚠️ O Express 5 tem outras mudanças que quebram código de v4 — o padrão de rota com * mudou, req.param() saiu, e alguns middlewares antigos não foram atualizados. Não migre projeto grande sem ler o guia oficial.
O que você pode apagar do projeto antigo
| dependência | ainda precisa? |
|---|---|
ts-node |
não, se o alvo é Node 22.18+ |
tsx |
não, para o caso comum |
nodemon |
não, use node --watch |
express-async-errors |
não, com Express 5 |
typescript |
sim — é ele que checa os tipos |
@types/node, @types/express |
sim |
Validação: o que este guia não cobre
Esta API aceita qualquer JSON e confere no braço, com if. Para projeto real você quer validação por schema — Zod é o padrão hoje, e ele deriva o tipo TypeScript do schema, então tipo e validação não saem de sincronia.
Também ficou de fora: banco de dados, autenticação e testes. Sobre a camada de segurança que toda API precisa antes de ir pro ar, escrevi o básico que quase ninguém faz. Se o Node ainda não está instalado na sua máquina, comece pelo guia do NVM, que cobre Linux, macOS e Windows. E se você está decidindo qual versão adotar, o que mudou no Node 22 ajuda.
Perguntas frequentes
O Node.js roda TypeScript nativamente?
Roda. Sem flag a partir do 22.18 na série 22 e do 23.6 na série 23 — no Node 24 LTS já é o padrão. Mas ele apenas apaga as anotações de tipo, não verifica nada.
Ainda preciso do ts-node ou do tsx em 2026?
Para executar, não — se o seu alvo é Node 22.18 ou superior. O node arquivo.ts cobre o caso comum. Você ainda precisa do pacote typescript instalado, porque é o tsc que checa os tipos.
O Node verifica os tipos quando roda um .ts?
Não. Um arquivo com const n: number = "texto" roda sem reclamar. A checagem continua sendo trabalho do tsc --noEmit, no editor ou no CI.
Por que dá "Cannot use import statement outside a module"?
Falta "type": "module" no package.json. É o erro mais comum ao montar o projeto, e a mensagem confunde porque parece problema de TypeScript quando é do sistema de módulos.
Por que meu enum não funciona com node arquivo.ts?
Porque enum não é sintaxe apagável — ele gera código em tempo de execução. Ou você roda com --experimental-transform-types, ou troca por um objeto as const. Ligar erasableSyntaxOnly no tsconfig avisa antes.
Preciso de express-async-errors no Express 5?
Não. No Express 5 um erro lançado dentro de handler async chega ao middleware de erro sozinho. Testado: a requisição devolve 500 e o servidor continua no ar.
Qual versão do Node usar para uma API nova?
A LTS, hoje a v24.19.0. Ela tem o suporte a TypeScript estável e o ciclo de manutenção mais longo.