Voltar para o Blog

Node.js 26 LTS no AWS Lambda com TypeScript: guia de migração

Use a janela entre o LTS do Node.js 26 e o GA do runtime nodejs26.x para testar sua Lambda nas duas versões, gerar bundles por alvo e homologar sem tocar na produção.

Node.js 26 LTS no AWS Lambda com TypeScript: guia de migração

No dia 28 de outubro de 2026 o projeto Node.js publicou a versão 26.13.0 e, com ela, a linha 26.x entrou oficialmente em Long Term Support, com o codinome "Lithium" e suporte previsto até o fim de abril de 2029. Para quem roda funções AWS Lambda em Node.js, essa data não é só uma curiosidade: a AWS condiciona a disponibilidade geral do runtime nodejs26.x justamente à entrada do Node.js 26 em Active LTS.

Hoje o runtime nodejs26.x já existe no Lambda, mas em public preview. A documentação oficial é clara: runtimes em preview não têm SLA nem suporte técnico e não devem ser usados em produção. A própria página de runtimes indica novembro de 2026 como mês-alvo para o lançamento do Node.js 26, e o blog de computação da AWS explica que o identificador é o mesmo no preview e no GA, então a função "gradua" sozinha quando o runtime sair do preview.

Isso cria uma janela perfeita para se preparar. Neste tutorial vamos pegar uma função Lambda pequena, escrita em TypeScript, e montar um fluxo reproduzível para validá-la no Node.js 24 e no Node.js 26 localmente, gerar o pacote com esbuild e testá-la no runtime nodejs26.x em uma função de homologação, mantendo a produção em nodejs24.x até o GA.

O problema: atualizar runtime sem descobrir o erro em produção

Trocar o runtime de uma Lambda parece uma mudança de uma linha, e tecnicamente é: basta alterar o identificador de nodejs24.x para nodejs26.x. O risco está no que essa linha arrasta junto. Uma nova versão major do Node.js traz uma nova versão do V8, mudanças de comportamento em APIs internas, remoção de recursos que estavam obsoletos e, às vezes, diferenças no próprio executável, como bibliotecas de sistema necessárias.

A documentação do Lambda reforça que a AWS não garante compatibilidade entre versões major e que a troca de runtime é uma operação conduzida pelo cliente. Em outras palavras, se algo quebrar, quem descobre é você. O objetivo deste tutorial é descobrir antes, em um ambiente onde quebrar é barato.

Há ainda um segundo problema, mais sutil: muitos projetos não sabem dizer em qual versão do Node.js os testes realmente rodaram. O desenvolvedor usa a versão que está instalada na máquina, o CI usa outra e a Lambda usa uma terceira. Vamos resolver isso rodando a mesma suíte de testes, de forma explícita, nas duas versões que importam agora.

O que você vai ter ao final

Ao terminar, você terá um projeto TypeScript com uma função que recebe um pedido via Lambda Function URL ou API Gateway HTTP API (payload versão 2.0), valida os itens e devolve o total em centavos. O projeto tem checagem de tipos com tsc, testes com o test runner nativo do Node.js executando arquivos .ts diretamente, um script de build com esbuild parametrizado pela versão alvo e um teste de fumaça do pacote gerado.

Você também terá a sequência de comandos da AWS CLI para criar uma função de homologação em nodejs26.x, invocá-la, publicar uma versão e conferir qual versão do runtime está em uso, além de um plano de rollback simples. A função de produção não é tocada em nenhum momento.

Pré-requisitos e versões

As versões abaixo foram conferidas em 29 de outubro de 2026, no blog oficial do Node.js e no registro do npm. O Node.js 26.13.0 é a primeira versão LTS da linha 26, e o Node.js 24.22.2, publicado no mesmo dia, é a versão mais recente da linha 24, que corresponde ao runtime nodejs24.x de produção.

  • Node.js 26.13.0 (LTS) e Node.js 24.22.2 (LTS), instalados lado a lado
  • TypeScript 7.0.2
  • esbuild 0.28.2
  • @types/aws-lambda 8.10.164 e @types/node 26.6.6
  • AWS CLI v2 configurado com um perfil de uma conta de testes
  • Utilitário zip para empacotar o código
  • Uma role IAM de execução para Lambda (com a policy gerenciada AWSLambdaBasicExecutionRole)

Um detalhe que vale para quem vai rodar o Node.js 26 em servidores Linux próprios ou em imagens de CI: as notas da versão 26.13.0 lembram que, desde o Node.js 25, os binários oficiais para Linux são ligados à biblioteca libatomic, que pode precisar ser instalada separadamente dependendo da distribuição. Isso não afeta o runtime gerenciado do Lambda, mas pode quebrar a sua pipeline antes mesmo de os testes começarem.

Os números de versão das dependências estão fixados no package.json de propósito. Em um tutorial, fixar versões é o que permite que outra pessoa chegue ao mesmo resultado. No seu projeto, ajuste para as versões vigentes quando for aplicar.

Arquitetura do fluxo

O fluxo tem quatro etapas. Primeiro, a checagem de tipos garante que o código compila sem emitir nada. Depois, os testes rodam duas vezes, uma em cada versão do Node.js, sem passo de compilação, porque as duas versões conseguem executar TypeScript com sintaxe apagável diretamente. Em seguida, o esbuild gera um único arquivo dist/index.mjs com o alvo da versão escolhida. Por fim, o pacote é testado localmente e enviado para uma função de homologação.

A separação entre homologação e produção é a decisão mais importante do desenho. Como o runtime em preview pode receber mudanças incompatíveis até o GA, a função de homologação serve para coletar sinais, não para atender tráfego real. Quando o runtime virar GA, você repete os mesmos passos na função de produção com muito mais confiança.

Estrutura de diretórios

lambda-node26/
├── package.json
├── tsconfig.json
├── .env.example
├── src/
│   ├── handler.ts
│   └── pedido.ts
├── test/
│   ├── handler.test.ts
│   └── pedido.test.ts
├── scripts/
│   ├── build.mjs
│   └── smoke.mjs
└── eventos/
    └── pedido-valido.json

Preparando o ambiente

Você precisa das duas versões do Node.js disponíveis. Qualquer gerenciador de versões serve; o que importa é conseguir chamar cada uma explicitamente. Nos exemplos, usamos as variáveis NODE24 e NODE26 apontando para os executáveis, o que deixa claro em qual versão cada comando roda.

mkdir lambda-node26 && cd lambda-node26
export NODE24=/caminho/para/node-v24.22.2/bin/node
export NODE26=/caminho/para/node-v26.13.0/bin/node
"$NODE24" -v
"$NODE26" -v

Crie também um arquivo de exemplo de variáveis de ambiente. Ele não contém segredos, apenas os nomes que os comandos da AWS CLI vão usar. Copie para .env, preencha com os dados da sua conta de testes e nunca versione o .env.

Arquivo: .env.example

AWS_PROFILE=conta-de-testes
AWS_REGION=sa-east-1
FUNCAO_HOMOLOG=pedidos-node26-homolog
ROLE_ARN=arn:aws:iam::000000000000:role/sua-role-de-execucao-lambda

Passo 1: o projeto TypeScript

O package.json declara o projeto como ES Module e define os scripts. Repare que o script de teste usa um padrão de glob entre aspas. Isso não é estética: ao preparar este tutorial, passar apenas o diretório (node --test test/) funcionou no Node.js 26.13.0, mas falhou no Node.js 24.22.2 com "Cannot find module". Com o glob explícito, as duas versões encontram os mesmos arquivos.

Arquivo: package.json

{
  "name": "lambda-node26",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "engines": { "node": ">=24" },
  "scripts": {
    "typecheck": "tsc --noEmit",
    "test": "node --test \"test/**/*.test.ts\"",
    "build": "node scripts/build.mjs",
    "package": "npm run build && cd dist && zip -q -r ../function.zip ."
  },
  "devDependencies": {
    "@types/aws-lambda": "8.10.164",
    "@types/node": "26.6.6",
    "esbuild": "0.28.2",
    "typescript": "7.0.2"
  }
}

O tsconfig.json tem duas opções que merecem atenção. A allowImportingTsExtensions permite importar ./pedido.ts com a extensão, que é o que o Node.js exige ao executar TypeScript diretamente. A erasableSyntaxOnly faz o compilador recusar construções que não podem ser simplesmente apagadas, como enum e namespace, que o Node.js não consegue executar sem uma etapa de transformação.

Arquivo: tsconfig.json

{
  "compilerOptions": {
    "target": "es2024",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "noEmit": true,
    "allowImportingTsExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true,
    "types": ["node"]
  },
  "include": ["src", "test"]
}

Instale as dependências com a versão 26 e confirme que a checagem de tipos passa. O tsc aqui não gera arquivos; quem gera o JavaScript final é o esbuild.

npm install
npm run typecheck

Passo 2: a regra de negócio isolada

Separar a regra de negócio do handler é o que torna a migração testável. O módulo pedido.ts não sabe nada sobre Lambda: recebe um valor desconhecido, valida e devolve um resultado tipado. Trabalhar com centavos inteiros evita os problemas clássicos de ponto flutuante em valores monetários.

Arquivo: src/pedido.ts

export type ItemPedido = {
  sku: string;
  quantidade: number;
  precoCentavos: number;
};

export type ResultadoValidacao =
  | { ok: true; itens: ItemPedido[]; totalCentavos: number }
  | { ok: false; erro: string };

export function validarPedido(entrada: unknown): ResultadoValidacao {
  if (typeof entrada !== "object" || entrada === null || !("itens" in entrada)) {
    return { ok: false, erro: "Corpo deve ter o campo itens" };
  }
  const { itens } = entrada as { itens: unknown };
  if (!Array.isArray(itens) || itens.length === 0) {
    return { ok: false, erro: "itens deve ser uma lista não vazia" };
  }
  const validos: ItemPedido[] = [];
  for (const item of itens) {
    const { sku, quantidade, precoCentavos } = (item ?? {}) as Record<string, unknown>;
    if (typeof sku !== "string" || sku.length === 0) {
      return { ok: false, erro: "sku inválido" };
    }
    if (!Number.isInteger(quantidade) || (quantidade as number) <= 0) {
      return { ok: false, erro: `quantidade inválida no sku ${sku}` };
    }
    if (!Number.isInteger(precoCentavos) || (precoCentavos as number) < 0) {
      return { ok: false, erro: `precoCentavos inválido no sku ${sku}` };
    }
    validos.push({ sku, quantidade: quantidade as number, precoCentavos: precoCentavos as number });
  }
  const totalCentavos = validos.reduce((soma, i) => soma + i.quantidade * i.precoCentavos, 0);
  return { ok: true, itens: validos, totalCentavos };
}

O tipo ResultadoValidacao é uma união discriminada pelo campo ok. Isso obriga quem consome a função a tratar o caso de erro antes de acessar itens ou totalCentavos, e o compilador acusa se alguém esquecer.

Passo 3: o handler da Lambda

O handler converte o evento HTTP em dados, delega a validação e monta a resposta. Ele trata corpo em Base64, que é como o Lambda entrega alguns payloads, e devolve process.version na resposta. Esse campo é o que vai provar, na homologação, que a função realmente rodou no Node.js 26.

Arquivo: src/handler.ts

import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
import { validarPedido } from "./pedido.ts";

function resposta(statusCode: number, corpo: unknown): APIGatewayProxyResultV2 {
  return {
    statusCode,
    headers: { "content-type": "application/json; charset=utf-8" },
    body: JSON.stringify(corpo),
  };
}

export async function handler(event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> {
  let dados: unknown;
  try {
    const bruto = event.isBase64Encoded
      ? Buffer.from(event.body ?? "", "base64").toString("utf8")
      : (event.body ?? "");
    dados = JSON.parse(bruto);
  } catch {
    return resposta(400, { erro: "JSON inválido" });
  }

  const resultado = validarPedido(dados);
  if (!resultado.ok) {
    return resposta(422, { erro: resultado.erro });
  }

  return resposta(200, {
    totalCentavos: resultado.totalCentavos,
    itens: resultado.itens.length,
    runtime: process.version,
  });
}

Expor a versão do runtime em uma API pública não é uma boa ideia em produção, porque entrega informação útil para quem procura vulnerabilidades. Aqui ela existe apenas para a função de homologação; antes de levar o código para produção, remova o campo ou proteja a rota.

Passo 4: testes rodando nas duas versões

Os testes usam apenas node:test e node:assert, sem nenhuma biblioteca extra. Como o Node.js 24 e o Node.js 26 executam TypeScript com sintaxe apagável diretamente, os arquivos .ts rodam sem compilação prévia.

Arquivo: test/pedido.test.ts

import { test } from "node:test";
import assert from "node:assert/strict";
import { validarPedido } from "../src/pedido.ts";

test("soma o total em centavos", () => {
  const r = validarPedido({ itens: [{ sku: "A1", quantidade: 2, precoCentavos: 1990 }] });
  assert.deepEqual(r, {
    ok: true,
    itens: [{ sku: "A1", quantidade: 2, precoCentavos: 1990 }],
    totalCentavos: 3980,
  });
});

test("rejeita quantidade fracionada", () => {
  const r = validarPedido({ itens: [{ sku: "A1", quantidade: 1.5, precoCentavos: 100 }] });
  assert.equal(r.ok, false);
});

test("rejeita lista vazia", () => {
  assert.deepEqual(validarPedido({ itens: [] }), { ok: false, erro: "itens deve ser uma lista não vazia" });
});

Arquivo: test/handler.test.ts

import { test } from "node:test";
import assert from "node:assert/strict";
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from "aws-lambda";
import { handler } from "../src/handler.ts";

function evento(body: string | undefined): APIGatewayProxyEventV2 {
  return { body, isBase64Encoded: false } as APIGatewayProxyEventV2;
}

test("responde 200 com o total", async () => {
  const r = (await handler(
    evento(JSON.stringify({ itens: [{ sku: "B2", quantidade: 3, precoCentavos: 500 }] })),
  )) as APIGatewayProxyStructuredResultV2;
  assert.equal(r.statusCode, 200);
  const corpo = JSON.parse(r.body ?? "{}");
  assert.equal(corpo.totalCentavos, 1500);
  assert.equal(corpo.runtime, process.version);
});

test("responde 400 para JSON quebrado", async () => {
  const r = (await handler(evento("{"))) as APIGatewayProxyStructuredResultV2;
  assert.equal(r.statusCode, 400);
});

Agora rode a suíte nas duas versões. Ao preparar este tutorial, os cinco testes passaram tanto no Node.js 24.22.2 quanto no Node.js 26.13.0. No seu projeto real, é aqui que aparecem as diferenças de comportamento que valem uma investigação antes da troca de runtime.

PATH="$(dirname "$NODE24"):$PATH" npm test
PATH="$(dirname "$NODE26"):$PATH" npm test

Se você usa CI, a mesma ideia vira uma matriz com as duas versões. O ponto é que a versão nunca fica implícita: cada execução diz exatamente onde rodou.

Passo 5: build com esbuild por versão alvo

O esbuild gera um único arquivo ESM, já com as dependências embutidas. O alvo é lido da variável NODE_TARGET, com node24 como padrão, para que o build de produção continue igual enquanto o Node.js 26 é avaliado.

Arquivo: scripts/build.mjs

import { build } from "esbuild";

const alvo = process.env.NODE_TARGET ?? "node24";

await build({
  entryPoints: ["src/handler.ts"],
  bundle: true,
  platform: "node",
  format: "esm",
  target: alvo,
  outfile: "dist/index.mjs",
  sourcemap: true,
  minify: false,
  logLevel: "info",
});

console.log(`Build gerado para ${alvo} em dist/index.mjs`);

O teste de fumaça importa o arquivo gerado, e não o código-fonte. Isso detecta problemas que só aparecem depois do bundle, como um import que o esbuild não conseguiu resolver.

Arquivo: scripts/smoke.mjs

import { handler } from "../dist/index.mjs";

const r = await handler({
  isBase64Encoded: false,
  body: JSON.stringify({ itens: [{ sku: "C3", quantidade: 1, precoCentavos: 4990 }] }),
});

console.log(r.statusCode, r.body);
if (r.statusCode !== 200) process.exit(1);
NODE_TARGET=node26 "$NODE26" scripts/build.mjs
"$NODE26" scripts/smoke.mjs

Na validação feita para este artigo, o teste de fumaça imprimiu o status 200 e o corpo com "runtime":"v26.13.0" quando executado com o Node.js 26, e "runtime":"v24.22.2" com o Node.js 24. É exatamente o mesmo arquivo empacotado, rodando em versões diferentes.

Executando na AWS: função de homologação em nodejs26.x

Os comandos abaixo não foram executados em uma conta AWS durante a preparação deste artigo; são comandos documentados da AWS CLI, e as saídas dependem da sua conta. Comece carregando as variáveis e gerando o pacote zip. O nome do arquivo dentro do zip precisa ser index.mjs para que o handler index.handler seja encontrado.

set -a; source .env; set +a
NODE_TARGET=node26 npm run package

Crie o arquivo de evento usado na invocação. Ele imita o formato 2.0 do payload HTTP, com o corpo como string JSON.

Arquivo: eventos/pedido-valido.json

{
  "version": "2.0",
  "isBase64Encoded": false,
  "body": "{\"itens\":[{\"sku\":\"C3\",\"quantidade\":2,\"precoCentavos\":4990}]}"
}

Agora crie a função de homologação já no runtime em preview e espere ela ficar ativa.

aws lambda create-function \
  --function-name "$FUNCAO_HOMOLOG" \
  --runtime nodejs26.x \
  --handler index.handler \
  --architectures arm64 \
  --role "$ROLE_ARN" \
  --zip-file fileb://function.zip

aws lambda wait function-active-v2 --function-name "$FUNCAO_HOMOLOG"

Invoque a função com o evento de exemplo. A opção --cli-binary-format raw-in-base64-out é necessária na AWS CLI v2 para enviar o payload como JSON puro.

aws lambda invoke \
  --function-name "$FUNCAO_HOMOLOG" \
  --cli-binary-format raw-in-base64-out \
  --payload file://eventos/pedido-valido.json \
  resposta.json

cat resposta.json

O esperado é um statusCode 200, com totalCentavos igual a 9980 e o campo runtime começando por v26. Se o campo mostrar outra versão, a função não está no runtime que você imagina, e vale conferir a configuração antes de seguir.

Para saber exatamente qual versão do runtime gerenciado o Lambda aplicou, consulte a configuração de gerenciamento de runtime. Durante o preview, guardar esse ARN ajuda a correlacionar um comportamento estranho com uma atualização do runtime.

aws lambda get-runtime-management-config --function-name "$FUNCAO_HOMOLOG"
aws lambda get-function-configuration --function-name "$FUNCAO_HOMOLOG" \
  --query '{runtime: Runtime, arquitetura: Architectures, estado: State}'

Atualizando código e publicando versões

A cada mudança, gere o pacote de novo, envie o código, espere a atualização terminar e publique uma versão imutável. Versões publicadas são o que torna o rollback possível, porque cada uma guarda o runtime e o código do momento em que foi criada.

NODE_TARGET=node26 npm run package
aws lambda update-function-code --function-name "$FUNCAO_HOMOLOG" --zip-file fileb://function.zip
aws lambda wait function-updated-v2 --function-name "$FUNCAO_HOMOLOG"
aws lambda publish-version --function-name "$FUNCAO_HOMOLOG" --description "teste nodejs26.x"

Se você preferir testar o Node.js 26 numa cópia de uma função que já existe em nodejs24.x, o caminho é trocar apenas o runtime na função de homologação. Nunca faça isso diretamente na função de produção enquanto o runtime estiver em preview.

aws lambda update-function-configuration --function-name "$FUNCAO_HOMOLOG" --runtime nodejs26.x
aws lambda wait function-updated-v2 --function-name "$FUNCAO_HOMOLOG"

O rollback é o mesmo comando com --runtime nodejs24.x, seguido de um novo build com NODE_TARGET=node24. Se você usa aliases, basta apontar o alias de volta para a última versão publicada que estava em nodejs24.x.

Erros comuns

O erro mais frequente é o Runtime.ImportModuleError dizendo que não encontrou index. Quase sempre o zip foi criado a partir da pasta errada, e o arquivo ficou em dist/index.mjs dentro do pacote, em vez de index.mjs na raiz. O script package entra na pasta dist antes de compactar justamente para evitar isso.

Outro tropeço comum é usar enum ou namespace no código. Com erasableSyntaxOnly ligado, o tsc acusa o problema; sem essa opção, o erro só aparece quando o Node.js tenta executar o arquivo .ts nos testes. Prefira uniões de strings literais e objetos as const.

Também é comum ver testes passando localmente e falhando no CI com Node.js 26 por falta da libatomic em imagens Linux enxutas. A mensagem costuma citar a biblioteca compartilhada ausente ao iniciar o executável. A correção é instalar o pacote da distribuição antes de rodar o Node.js, conforme as notas oficiais da versão 26.13.0.

Por fim, lembre que a função em preview pode mudar de comportamento sem aviso entre uma semana e outra. Se um teste que passava começar a falhar sem mudança no seu código, compare o ARN de versão do runtime retornado por get-runtime-management-config com o da execução anterior e, se fizer sentido, reporte no issue de feedback do Node.js 26 indicado pela AWS.

Segurança

Mantenha a função de homologação em uma conta separada ou, no mínimo, sem acesso a dados de produção. A role de execução deste exemplo só precisa da policy gerenciada de logs; qualquer permissão adicional deve ser a mínima necessária para o teste.

Não coloque credenciais no .env do projeto. A AWS CLI deve ler as credenciais do perfil configurado ou de um mecanismo de login como o IAM Identity Center. O .env guarda apenas nomes de recursos, e mesmo assim não deve ir para o repositório.

Se você expuser a função por uma Function URL para testar via HTTP, prefira o tipo de autenticação AWS_IAM em vez de deixá-la pública. Uma URL aberta em homologação costuma ser esquecida e acaba virando uma porta de entrada.

Performance e custos

A AWS informa que funções em runtimes de preview são cobradas pelas tarifas normais do Lambda, sem custo adicional. Para uma função de homologação invocada algumas vezes por dia, o custo tende a ser irrisório, mas lembre-se dos logs no CloudWatch, que são cobrados à parte e devem ter um período de retenção definido.

Não tiramos conclusões de desempenho neste artigo, e você também não deveria tirá-las durante o preview. A AWS deixa claro que o padrão de qualidade e desempenho do GA só se aplica depois da graduação. Se quiser comparar tempos de inicialização entre Node.js 24 e 26, faça isso depois do GA, com o mesmo pacote, a mesma memória e a mesma arquitetura.

Limitações

Este tutorial cobre funções empacotadas como arquivo zip. Para funções em imagem de contêiner, a AWS publica uma imagem base de preview própria, e o fluxo de build é diferente. A função de exemplo também não usa o AWS SDK; se a sua usa, inclua os módulos do SDK no pacote em vez de depender da versão embutida no runtime, como recomenda a documentação do Lambda.

A validação local deste artigo cobriu checagem de tipos, testes e o teste de fumaça do bundle nas duas versões do Node.js. A criação da função, a invocação e a troca de runtime na AWS não foram executadas, e o empacotamento com zip também não foi testado no ambiente usado para escrever o texto.

Conclusão

A entrada do Node.js 26 em LTS, em 28 de outubro de 2026, abre a contagem regressiva para o GA do runtime nodejs26.x no Lambda. Quem usa esse intervalo para rodar os testes nas duas versões, gerar bundles por alvo e exercitar o runtime em uma função de homologação chega ao GA com a migração praticamente pronta.

O fluxo que montamos é pequeno de propósito: duas versões explícitas, um build parametrizado, um teste de fumaça do pacote e uma função isolada. Quando o runtime sair do preview, a mudança na produção volta a ser aquilo que deveria ser, uma linha de configuração, só que agora com evidências de que ela funciona.

Referências

Precisa implementar isso na sua empresa?

Quer aplicar essas práticas de engenharia, dados ou inteligência artificial no seu produto? Solicite um diagnóstico técnico sem compromisso.

Solicitar orçamento