API do ChatGPT com Node.js: guia com Responses API e streaming

API do ChatGPT com Node.js permite criar um backend que envia mensagens, imagens, arquivos ou tarefas à OpenAI API e devolve os resultados ao seu aplicativo. A integração atual deve usar o SDK oficial no servidor e manter a chave fora do navegador.

Este tutorial mostra uma chamada simples com a Responses API, um endpoint HTTP construído com módulos nativos do Node.js, tratamento de erros, continuação de conversa e streaming. Os exemplos usam ECMAScript Modules e um model ID configurável por variável de ambiente.

Documentação e código revisados em 10 de outubro de 2026. Os exemplos não foram executados neste artigo; valide dependências, modelo, permissões e comportamento no seu próprio ambiente.

O que você vai construir

  • uma chamada direta à OpenAI API;
  • um endpoint POST /api/responder;
  • validação básica do JSON e do tamanho da mensagem;
  • tratamento de autenticação, conexão e rate limit;
  • streaming pelo terminal e por Server-Sent Events;
  • continuação de conversa com previous_response_id.
Frontend
   |
   | POST /api/responder
   v
Servidor Node.js
   - valida entrada
   - autentica usuário
   - protege a chave
   - chama a OpenAI
   |
   v
Responses API

Pré-requisitos

ItemRecomendação
Node.jsVersão em Active LTS ou Maintenance LTS
Versão usada como referênciaNode.js 24 LTS em 10/10/2026
ContaProjeto na OpenAI Platform com faturamento configurado
SDKPacote oficial openai
ChaveOPENAI_API_KEY no ambiente do servidor
MódulosESM por meio de "type": "module"

A página de versões do Node.js recomenda usar versões LTS em aplicações de produção. Versões Current podem receber mudanças com maior frequência, e versões EOL não recebem a manutenção esperada.

1. Crie o projeto

mkdir openai-node
cd openai-node

npm init -y
npm install openai dotenv
npm pkg set type=module

O pacote openai é o SDK oficial para TypeScript e JavaScript. O pacote dotenv carrega variáveis de um arquivo local durante o desenvolvimento.

O package.json terá uma configuração semelhante a:

{
  "name": "openai-node",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node server.mjs",
    "test-api": "node exemplo.mjs"
  },
  "dependencies": {
    "dotenv": "...",
    "openai": "..."
  }
}

Os números de versão foram omitidos porque devem vir do seu arquivo de lock após a instalação. Em produção, versione package-lock.json e teste atualizações antes de publicar.

2. Configure a chave da API

Crie um arquivo .env:

OPENAI_API_KEY=sua_chave_aqui
OPENAI_MODEL=gpt-6-luna
PORT=3000
FRONTEND_ORIGIN=http://localhost:5173

Crie um .gitignore:

node_modules/
.env
npm-debug.log*

O SDK lê OPENAI_API_KEY automaticamente. Nunca use dangerouslyAllowBrowser ou coloque a chave em código entregue ao navegador apenas para simplificar um protótipo público.

3. Faça a primeira chamada

Crie exemplo.mjs:

import "dotenv/config";
import OpenAI from "openai";

const client = new OpenAI();

const model =
  process.env.OPENAI_MODEL ?? "gpt-6-luna";

async function main() {
  const response = await client.responses.create({
    model,
    instructions:
      "Responda em português do Brasil, com clareza.",
    input:
      "Explique o que é Node.js em três frases.",
    store: false,
  });

  console.log(response.output_text);
  console.log({
    id: response.id,
    model: response.model,
    usage: response.usage,
  });
}

main().catch((error) => {
  console.error("Falha na chamada:", error);
  process.exitCode = 1;
});

Execute:

node exemplo.mjs

A chamada utiliza client.responses.create(), interface principal descrita na documentação do SDK JavaScript. O campo output_text agrega o texto produzido.

Como tratar erros na chamada simples

import "dotenv/config";
import OpenAI from "openai";

const client = new OpenAI();

try {
  const response = await client.responses.create({
    model:
      process.env.OPENAI_MODEL ?? "gpt-6-luna",
    input: "Responda apenas com a palavra: teste.",
    store: false,
  });

  console.log(response.output_text);
} catch (error) {
  if (error instanceof OpenAI.AuthenticationError) {
    console.error(
      "Chave inválida ou sem permissão."
    );
  } else if (error instanceof OpenAI.RateLimitError) {
    console.error(
      "Limite temporário ou financeiro atingido."
    );
  } else if (
    error instanceof OpenAI.APIConnectionError
  ) {
    console.error(
      "Não foi possível conectar à OpenAI API."
    );
  } else if (error instanceof OpenAI.APIError) {
    console.error(
      "Erro retornado pela OpenAI API:",
      error.status,
      error.message
    );
  } else {
    throw error;
  }
}

Não envie error.message diretamente ao usuário final. A mensagem pode revelar detalhes de infraestrutura, identificadores ou configuração. Registre internamente apenas o necessário e devolva uma resposta controlada.

4. Crie um backend HTTP sem framework externo

O exemplo seguinte usa node:http, módulo nativo do Node.js. Ele cria:

  • GET /health;
  • POST /api/responder;
  • limite de 64 KB para o corpo HTTP;
  • limite de 8 mil caracteres para a mensagem;
  • CORS para uma origem configurada;
  • erros JSON consistentes.

Crie server.mjs:

import "dotenv/config";
import { createServer } from "node:http";
import OpenAI from "openai";

const client = new OpenAI();

const model =
  process.env.OPENAI_MODEL ?? "gpt-6-luna";

const port = Number.parseInt(
  process.env.PORT ?? "3000",
  10
);

const allowedOrigin =
  process.env.FRONTEND_ORIGIN ?? "";

const MAX_BODY_BYTES = 64 * 1024;
const MAX_MESSAGE_LENGTH = 8_000;

function sendJson(response, status, payload) {
  const body = JSON.stringify(payload);

  response.writeHead(status, {
    "Content-Type":
      "application/json; charset=utf-8",
    "Content-Length":
      Buffer.byteLength(body),
  });

  response.end(body);
}

function createHttpError(statusCode, message) {
  const error = new Error(message);
  error.statusCode = statusCode;
  return error;
}

function applyCors(request, response) {
  const origin = request.headers.origin;

  if (allowedOrigin && origin === allowedOrigin) {
    response.setHeader(
      "Access-Control-Allow-Origin",
      origin
    );
    response.setHeader("Vary", "Origin");
    response.setHeader(
      "Access-Control-Allow-Headers",
      "Content-Type, Authorization"
    );
    response.setHeader(
      "Access-Control-Allow-Methods",
      "GET, POST, OPTIONS"
    );
  }
}

async function readJson(request) {
  const chunks = [];
  let totalBytes = 0;

  for await (const chunk of request) {
    totalBytes += chunk.length;

    if (totalBytes > MAX_BODY_BYTES) {
      throw createHttpError(
        413,
        "O corpo da solicitação é muito grande."
      );
    }

    chunks.push(chunk);
  }

  if (chunks.length === 0) {
    return {};
  }

  try {
    const text = Buffer.concat(chunks)
      .toString("utf8");

    return JSON.parse(text);
  } catch {
    throw createHttpError(
      400,
      "O corpo precisa ser um JSON válido."
    );
  }
}

const server = createServer(
  async (request, response) => {
    applyCors(request, response);

    if (request.method === "OPTIONS") {
      response.writeHead(204);
      response.end();
      return;
    }

    const url = new URL(
      request.url ?? "/",
      `http://${request.headers.host ?? "localhost"}`
    );

    if (
      request.method === "GET" &&
      url.pathname === "/health"
    ) {
      sendJson(response, 200, { status: "ok" });
      return;
    }

    if (
      request.method !== "POST" ||
      url.pathname !== "/api/responder"
    ) {
      sendJson(response, 404, {
        erro: "Rota não encontrada.",
      });
      return;
    }

    try {
      const body = await readJson(request);

      const mensagem =
        typeof body.mensagem === "string"
          ? body.mensagem.trim()
          : "";

      if (!mensagem) {
        sendJson(response, 400, {
          erro:
            "O campo mensagem é obrigatório.",
        });
        return;
      }

      if (
        mensagem.length > MAX_MESSAGE_LENGTH
      ) {
        sendJson(response, 400, {
          erro:
            "A mensagem ultrapassa o limite permitido.",
        });
        return;
      }

      const result =
        await client.responses.create({
          model,
          instructions:
            "Responda em português do Brasil. " +
            "Não invente dados ou ações concluídas.",
          input: mensagem,
          store: false,
        });

      const text = result.output_text.trim();

      if (!text) {
        sendJson(response, 502, {
          erro:
            "O modelo não retornou conteúdo textual.",
        });
        return;
      }

      sendJson(response, 200, {
        resposta: text,
        model: result.model,
        response_id: result.id,
      });
    } catch (error) {
      if (
        error instanceof OpenAI.RateLimitError
      ) {
        sendJson(response, 429, {
          erro:
            "Limite temporário atingido. " +
            "Tente novamente depois.",
        });
        return;
      }

      if (
        error instanceof OpenAI.AuthenticationError
      ) {
        sendJson(response, 500, {
          erro:
            "Falha na configuração do serviço.",
        });
        return;
      }

      if (
        error instanceof OpenAI.APIConnectionError
      ) {
        sendJson(response, 503, {
          erro:
            "Não foi possível conectar ao serviço.",
        });
        return;
      }

      if (error instanceof OpenAI.APIError) {
        sendJson(response, 502, {
          erro:
            "A OpenAI API retornou um erro.",
        });
        return;
      }

      const status =
        Number.isInteger(error.statusCode)
          ? error.statusCode
          : 500;

      sendJson(response, status, {
        erro:
          status >= 500
            ? "Erro interno do servidor."
            : error.message,
      });
    }
  }
);

server.listen(port, "0.0.0.0", () => {
  console.log(
    `Servidor em http://localhost:${port}`
  );
});

O limite do corpo protege contra solicitações excessivamente grandes, mas não substitui rate limiting, autenticação ou limite financeiro.

5. Execute e teste o servidor

npm start

Teste a rota de saúde:

curl http://localhost:3000/health

Teste a geração:

curl -X POST http://localhost:3000/api/responder \
  -H "Content-Type: application/json" \
  -d '{
    "mensagem": "Explique a diferença entre Node.js e JavaScript."
  }'

A resposta terá estrutura semelhante a:

{
  "resposta": "JavaScript é uma linguagem...",
  "model": "gpt-6-luna",
  "response_id": "resp_..."
}

Por que a chave não deve ficar no frontend?

Código JavaScript executado no navegador pode ser lido pelo usuário. Uma chave incorporada ao bundle, ao HTML ou a uma variável pública pode ser copiada e usada para consumir seus créditos.

O padrão correto é:

  • frontend autentica o usuário no seu sistema;
  • frontend chama seu backend;
  • backend autoriza a tarefa e verifica limites;
  • backend usa a chave da OpenAI;
  • backend devolve apenas o resultado permitido.

CORS não protege a chave. Ele é uma política aplicada por navegadores, não uma barreira contra scripts, servidores ou clientes HTTP externos.

Como adicionar streaming no terminal

Crie stream.mjs:

import "dotenv/config";
import OpenAI from "openai";

const client = new OpenAI();

const stream =
  await client.responses.create({
    model:
      process.env.OPENAI_MODEL ??
      "gpt-6-luna",
    input:
      "Explique streaming em APIs com um exemplo curto.",
    store: false,
    stream: true,
  });

for await (const event of stream) {
  if (
    event.type ===
    "response.output_text.delta"
  ) {
    process.stdout.write(event.delta);
  } else if (
    event.type === "response.completed"
  ) {
    process.stdout.write("\n");
  } else if (event.type === "error") {
    console.error(event);
  }
}

Execute:

node stream.mjs

O guia de streaming da OpenAI recomenda Responses API porque seus eventos possuem tipos específicos. Isso facilita separar texto, conclusão, ferramenta e erro.

Como transmitir o texto por SSE no servidor

O trecho abaixo mostra a parte central de uma rota SSE. Ele pressupõe que a mensagem já foi validada e que o cliente client já existe:

async function streamToResponse(
  response,
  mensagem
) {
  response.writeHead(200, {
    "Content-Type":
      "text/event-stream; charset=utf-8",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "X-Accel-Buffering": "no",
  });

  try {
    const stream =
      await client.responses.create({
        model,
        instructions:
          "Responda em português do Brasil.",
        input: mensagem,
        store: false,
        stream: true,
      });

    for await (const event of stream) {
      if (
        event.type ===
        "response.output_text.delta"
      ) {
        const payload = JSON.stringify({
          delta: event.delta,
        });

        response.write(
          `data: ${payload}\n\n`
        );
      } else if (
        event.type ===
        "response.completed"
      ) {
        response.write(
          "event: done\ndata: {}\n\n"
        );
      }
    }
  } catch {
    response.write(
      "event: error\n" +
      'data: {"mensagem":"Falha no streaming."}\n\n'
    );
  } finally {
    response.end();
  }
}

Como os cabeçalhos já foram enviados, falhas posteriores devem ser comunicadas como eventos. O frontend precisa tratar done, error, cancelamento e perda de conexão.

Streaming melhora a experiência percebida, mas pode mostrar texto antes de uma verificação completa. Em aplicações sensíveis, considere acumular e revisar a resposta antes de exibi-la.

Como continuar uma conversa

Use previous_response_id quando a próxima solicitação precisar do contexto da resposta anterior:

const primeira =
  await client.responses.create({
    model: "gpt-6-luna",
    input:
      "Meu produto é uma plataforma para clínicas.",
  });

const segunda =
  await client.responses.create({
    model: "gpt-6-luna",
    previous_response_id: primeira.id,
    input:
      "Crie três mensagens de onboarding.",
  });

console.log(segunda.output_text);

O backend deve associar o ID ao usuário autenticado. Não aceite um ID arbitrário fornecido pelo navegador sem verificar propriedade e permissão.

Para conversas duráveis entre sessões, avalie a Conversations API. Defina retenção, exclusão, autorização e custo do histórico antes de persistir dados.

Como obter JSON estruturado

Não dependa apenas de uma instrução como “responda em JSON”. Para integrações que exigem campos previsíveis, use Structured Outputs com um schema suportado e valide novamente no Node.js.

Uma resposta estruturalmente válida ainda pode conter dados incorretos. Por exemplo, o campo data pode respeitar o formato esperado, mas trazer uma data inventada.

A validação deve ocorrer em duas camadas:

  1. o schema restringe a forma da saída;
  2. o backend valida regras de negócio e fontes.

Como escolher o model ID

O catálogo atual de modelos indica opções diferentes para capacidade, equilíbrio e escala.

ModeloPerfilUso provável
gpt-6-astraMaior capacidadeRaciocínio, programação e tarefas complexas
gpt-6.1-solEquilíbrioAplicações profissionais com custo controlado
gpt-6-lunaEficiênciaClassificação, extração e alto volume

Mantenha o model ID em configuração do servidor. Não permita que o usuário escolha qualquer modelo sem autorização.

const MODEL_BY_PLAN = {
  basico: "gpt-6-luna",
  profissional: "gpt-6.1-sol",
  avancado: "gpt-6-astra",
};

Esse mapa é apenas um exemplo de controle interno. Sua escolha deve ser baseada em avaliações reais e atualizada quando modelos forem substituídos.

Como controlar custos

  • limite corpo HTTP e comprimento da mensagem;
  • controle chamadas por usuário;
  • use um modelo eficiente para tarefas simples;
  • não reenvie histórico irrelevante;
  • preserve prefixos estáveis para prompt caching;
  • defina tamanho máximo de saída adequado;
  • registre response.usage;
  • limite número de novas tentativas;
  • configure alertas e limites no projeto.
custo estimado =
entrada
+ gravações e leituras de cache
+ saída
+ ferramentas
+ armazenamento
+ novas tentativas
+ revisão humana

Consulte os preços atuais da OpenAI API. Não coloque valores fixos em regras permanentes sem um processo de atualização.

Rate limits e tentativas

Um erro 429 pode representar rate limit temporário, aumento rápido de tráfego, créditos esgotados ou limite de gasto. Analise o código específico do erro.

Para limites temporários:

  • respeite Retry-After;
  • use backoff exponencial com jitter;
  • limite a quantidade de tentativas;
  • evite rajadas desnecessárias;
  • use filas para trabalhos não urgentes.

Não repita erros de autenticação, créditos ou limite financeiro esperando que eles desapareçam. Corrija a configuração.

Segurança para produção

  • autentique usuários antes da geração;
  • mantenha a chave no servidor;
  • use projetos separados por ambiente;
  • aplique permissões mínimas à chave;
  • limite entrada e saída;
  • valide JSON e tipos;
  • não confie em chamadas de ferramenta sem validação;
  • exija aprovação para pagamento, exclusão ou publicação;
  • não registre segredos;
  • faça testes de prompt injection;
  • use moderação e revisão humana quando apropriado;
  • revogue imediatamente uma chave exposta.

Privacidade e armazenamento

A OpenAI informa que dados enviados pela API não são usados para treinamento sem consentimento explícito. Isso não significa que seu sistema não armazena dados.

Revise:

  • logs do Node.js;
  • logs do provedor de hospedagem;
  • banco de dados da aplicação;
  • objetos Response e Conversations;
  • ferramentas e conectores;
  • serviços de monitoramento;
  • backups;
  • política de exclusão.

Minimize o conteúdo enviado e evite registrar prompts completos sem uma necessidade documentada. Não presuma conformidade automática com a LGPD por usar determinado modelo ou plano.

Como preparar o deploy

  • use Node.js LTS;
  • mantenha package-lock.json versionado;
  • execute atrás de HTTPS;
  • use gerenciador de processos ou plataforma com reinício automático;
  • configure timeouts;
  • implemente health checks;
  • use gerenciador de segredos;
  • aplique autenticação e rate limit;
  • monitore memória, CPU, erros e latência;
  • acompanhe consumo da OpenAI API;
  • tenha uma estratégia de fallback controlado.

Não use o servidor do tutorial como proxy público anônimo. Sem autenticação e limite, terceiros podem consumir sua conta e gerar conteúdo em seu nome.

Erros frequentes

Cannot use import statement outside a module

Confirme "type": "module" no package.json ou use a extensão .mjs. A documentação do Node.js descreve os marcadores aceitos para ECMAScript Modules.

OPENAI_API_KEY is missing

Confira o arquivo .env, o diretório de execução e o carregamento de dotenv/config. Reinicie o processo depois de alterar variáveis.

401 ou AuthenticationError

A chave pode estar incorreta, revogada, ligada a outro projeto ou sem permissão para o recurso.

429 ou RateLimitError

Verifique o código do erro. Pode ser limite temporário, saldo, gasto ou uso mensal. Nem todo 429 deve ser repetido automaticamente.

O frontend não consegue chamar o backend

Confira origem, protocolo e porta. Autorize apenas a origem necessária e trate requisições OPTIONS.

O texto chega apenas no final

A chamada normal não transmite deltas. Ative stream: true e processe eventos response.output_text.delta.

Checklist final

  • Node.js em versão LTS;
  • SDK oficial instalado;
  • ECMAScript Modules configurados;
  • chave em variável de ambiente;
  • backend entre navegador e OpenAI;
  • autenticação própria;
  • limite de corpo e mensagem;
  • CORS restrito;
  • tratamento de erros;
  • rate limit e controle financeiro;
  • logs sem segredos;
  • monitoramento de tokens;
  • testes de segurança e qualidade;
  • revisão humana em tarefas sensíveis.

A chamada inicial da API do ChatGPT com Node.js é curta, mas uma aplicação confiável precisa de backend, autenticação, validação, controle de custo e observabilidade. Comece pelo SDK oficial e pela Responses API, mantenha o model ID configurável e trate cada saída como um resultado que precisa ser validado dentro da sua regra de negócio.

Fontes oficiais verificadas