Como criar um aplicativo com a DeepSeek API

Neste tutorial, você vai criar um aplicativo web simples com React no frontend e Node.js com Express no backend. O navegador envia a mensagem apenas ao seu servidor; o servidor valida a entrada, acrescenta a chave da API e faz a chamada para a DeepSeek. O resultado é um ponto de partida funcional, com limites claros para você não confundir um protótipo com uma implantação pronta para qualquer risco.

Status da API em 19 de julho de 2026: use deepseek-v4-flash ou deepseek-v4-pro. Os aliases deepseek-chat e deepseek-reasoner serão desativados em 24 de julho de 2026, às 15:59 UTC. Durante a transição, eles apontam para V4 Flash em Non-Thinking e Thinking, respectivamente. Não crie código novo com esses aliases.

Regra principal: nunca coloque DEEPSEEK_API_KEY em React, JavaScript entregue ao navegador, aplicativo móvel distribuído ou repositório público. Os Termos da Open Platform também exigem que a chave não seja exposta em código client-side.

Última verificação técnica: 19 de julho de 2026. Este é um guia independente, sem afiliação, autorização ou endosso da DeepSeek.

O que este tutorial cobre

  • interface React com envio, carregamento, erro e resposta acessível;
  • rota POST /api/chat no Express;
  • chamada server-side para POST https://api.deepseek.com/chat/completions;
  • allowlist dos dois IDs V4 hospedados;
  • Thinking Mode configurado explicitamente;
  • validação de ambiente e entrada com Zod;
  • rate limit do próprio aplicativo, timeout e erros sanitizados;
  • user_id pseudônimo, sem e-mail ou outro dado pessoal;
  • build do frontend servido pelo mesmo backend.

O foco aqui é a arquitetura completa de um aplicativo pequeno. Para todos os campos, formatos e respostas do endpoint, use a página de Chat Completions. Para visão geral de autenticação, compatibilidade e endpoints, consulte DeepSeek API.

Arquitetura segura do aplicativo

CamadaResponsabilidadeContém a API key?
Navegador / ReactColetar mensagem, mostrar estado e renderizar textoNão
Backend / ExpressAutenticar, validar, limitar, chamar o provedor e filtrar errosSim, via ambiente ou secret manager
DeepSeek APIProcessar a entrada e gerar a saídaRecebe a credencial no header Bearer
Banco opcionalContas, consentimentos e histórico com retenção definidaNão precisa armazenar a API key

O backend é uma fronteira de confiança. Ele impede a cópia direta da chave pelo navegador e fornece um ponto central para autenticação, quotas, política de dados, troca de modelo e desligamento emergencial. O frontend nunca escolhe livremente o ID enviado ao provedor.

Qual modelo usar no aplicativo

NecessidadeConfiguração inicialObservação
Chat rápido e frequentedeepseek-v4-flash com Thinking desativadoBom ponto de partida para medir latência e custo
Raciocínio com menor custo relativodeepseek-v4-flash com Thinking ativadoÉ a correspondência da transição do antigo deepseek-reasoner
Tarefa complexa após avaliaçãodeepseek-v4-proUpgrade opcional; não é o substituto direto do alias antigo

Comece com uma configuração explícita e promova tarefas específicas ao Pro somente depois de medir qualidade, latência e custo. A página de DeepSeek V4 explica as diferenças; a página de preços mantém o contexto de cobrança separado deste tutorial.

Pré-requisitos

  • Node.js 20 ou superior, para usar fetch e AbortSignal.timeout nativos;
  • conta na plataforma da DeepSeek, saldo disponível e API key;
  • terminal, editor e noções básicas de JavaScript e React;
  • ambiente local no qual você possa criar variáveis secretas.

Crie a chave somente no painel oficial. Não cole uma chave real em tutorial, captura de tela, issue, conversa de suporte, ferramenta de analytics ou commit. Se houver exposição, revogue e substitua a credencial; apagar o commit não garante que o segredo deixou de existir no histórico.

1. Criar o projeto

mkdir app-deepseek
cd app-deepseek

npm init -y
npm install express dotenv helmet express-rate-limit zod

npm create vite@latest client -- --template react
npm --prefix client install

npm pkg set type=module
npm pkg set scripts.dev:server="node --watch server.mjs"
npm pkg set scripts.dev:client="npm --prefix client run dev"
npm pkg set scripts.build="npm --prefix client run build"
npm pkg set scripts.start="node server.mjs"

A estrutura usada nas próximas etapas é:

app-deepseek/
├── client/
│   ├── src/App.jsx
│   ├── src/index.css
│   └── vite.config.js
├── server.mjs
├── .env
├── .env.example
├── .gitignore
└── package.json

2. Configurar variáveis e arquivos ignorados

Crie .env.example sem valores secretos:

DEEPSEEK_API_KEY=substitua_no_arquivo_env_local
DEEPSEEK_MODEL=deepseek-v4-flash
DEEPSEEK_THINKING=disabled
APP_USER_ID_SECRET=use_ao_menos_32_caracteres_aleatorios
PORT=3000

Copie esse arquivo para .env e preencha os valores locais. Gere APP_USER_ID_SECRET com um gerador criptográfico; ele será usado para transformar o identificador aleatório do navegador em um user_id pseudônimo. Esse mecanismo não autentica ninguém.

node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"

Crie .gitignore:

.env
node_modules/
client/node_modules/
client/dist/
npm-debug.log*

3. Backend completo com Node.js e Express

Crie server.mjs na raiz:

import "dotenv/config";
import crypto from "node:crypto";
import path from "node:path";
import { fileURLToPath } from "node:url";
import express from "express";
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import { z } from "zod";

const envSchema = z.object({
  DEEPSEEK_API_KEY: z.string().min(20),
  DEEPSEEK_MODEL: z
    .enum(["deepseek-v4-flash", "deepseek-v4-pro"])
    .default("deepseek-v4-flash"),
  DEEPSEEK_THINKING: z.enum(["enabled", "disabled"]).default("disabled"),
  APP_USER_ID_SECRET: z.string().min(32),
  PORT: z.coerce.number().int().min(1).max(65535).default(3000),
});

const env = envSchema.parse(process.env);

const requestSchema = z.object({
  message: z.string().trim().min(1).max(4000),
  sessionId: z.string().regex(/^[a-f0-9]{32}$/i),
}).strict();

class UpstreamError extends Error {
  constructor(status, message) {
    super(message);
    this.name = "UpstreamError";
    this.status = status;
  }
}

function makeUserId(sessionId) {
  const digest = crypto
    .createHmac("sha256", env.APP_USER_ID_SECRET)
    .update(sessionId)
    .digest("base64url");

  return `web_${digest}`;
}

async function createCompletion({ message, userId }) {
  const payload = {
    model: env.DEEPSEEK_MODEL,
    messages: [
      {
        role: "system",
        content: "Responda em português brasileiro. Se não souber, diga que não sabe.",
      },
      { role: "user", content: message },
    ],
    thinking: { type: env.DEEPSEEK_THINKING },
    max_tokens: 800,
    stream: false,
    user_id: userId,
  };

  if (env.DEEPSEEK_THINKING === "enabled") {
    payload.reasoning_effort = "high";
  }

  const response = await fetch(
    "https://api.deepseek.com/chat/completions",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${env.DEEPSEEK_API_KEY}`,
      },
      body: JSON.stringify(payload),
      signal: AbortSignal.timeout(90_000),
    },
  );

  if (!response.ok) {
    throw new UpstreamError(response.status, "Falha na API do provedor");
  }

  let data;
  try {
    data = await response.json();
  } catch {
    throw new UpstreamError(502, "Resposta inválida do provedor");
  }

  const choice = data?.choices?.[0];
  if (!choice?.message) {
    throw new UpstreamError(502, "Resposta incompleta do provedor");
  }

  return choice;
}

const app = express();
app.disable("x-powered-by");
app.use(helmet());
app.use(express.json({ limit: "16kb", type: "application/json" }));

app.use(
  "/api",
  rateLimit({
    windowMs: 60_000,
    limit: 20,
    standardHeaders: true,
    legacyHeaders: false,
    message: { error: "Muitas solicitações. Aguarde um minuto." },
  }),
);

app.post("/api/chat", async (req, res) => {
  const parsed = requestSchema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({ error: "Mensagem ou sessão inválida." });
  }

  try {
    const choice = await createCompletion({
      message: parsed.data.message,
      userId: makeUserId(parsed.data.sessionId),
    });

    const unexpectedToolCalls =
      Array.isArray(choice.message.tool_calls) &&
      choice.message.tool_calls.length > 0;

    if (choice.finish_reason !== "stop" || unexpectedToolCalls) {
      console.error("Resposta não finalizada pelo provedor", {
        finishReason: choice.finish_reason || "missing",
        unexpectedToolCalls,
      });

      return res.status(502).json({
        error: "O provedor não concluiu uma resposta utilizável.",
      });
    }

    const answer = choice.message.content;
    if (typeof answer !== "string" || !answer.trim()) {
      return res.status(502).json({ error: "Resposta vazia do provedor." });
    }

    return res.json({ answer });
  } catch (error) {
    const upstreamStatus = error instanceof UpstreamError ? error.status : null;

    console.error("Falha ao gerar resposta", {
      type: error?.name || "UnknownError",
      upstreamStatus,
    });

    if (upstreamStatus === 401 || upstreamStatus === 402) {
      return res.status(503).json({ error: "Serviço indisponível." });
    }

    if (upstreamStatus === 429 || upstreamStatus === 500 || upstreamStatus === 503) {
      return res.status(503).json({
        error: "Serviço ocupado. Tente novamente em instantes.",
      });
    }

    if (error?.name === "TimeoutError") {
      return res.status(504).json({ error: "O provedor demorou para responder." });
    }

    return res.status(502).json({ error: "Não foi possível gerar a resposta." });
  }
});

const currentFile = fileURLToPath(import.meta.url);
const currentDir = path.dirname(currentFile);
const clientDist = path.join(currentDir, "client", "dist");

app.use(express.static(clientDist));
app.get("/", (req, res) => {
  res.sendFile(path.join(clientDist, "index.html"));
});

app.listen(env.PORT, () => {
  console.log(`Aplicativo disponível na porta ${env.PORT}`);
});

O que o backend protege

  • a chave existe apenas no processo do servidor;
  • o ambiente aceita somente deepseek-v4-flash ou deepseek-v4-pro;
  • o corpo precisa ser JSON pequeno, com mensagem e sessão no formato esperado;
  • o user_id é derivado por HMAC e não contém o identificador original;
  • logs registram tipo e status, não chave, prompt ou corpo bruto do erro;
  • timeout e mensagens genéricas evitam conexões indefinidas e vazamento de detalhes.

O limite de 20 chamadas por minuto é uma regra ilustrativa do seu aplicativo, não um limite oficial da DeepSeek. Em produção, autentique contas e aplique quota por usuário ou organização. Se houver proxy ou load balancer, configure trust proxy somente para a topologia real; uma configuração genérica pode permitir falsificação do IP usado pelo rate limiter.

Como este endpoint não declara ferramentas, ele só aceita a resposta como concluída quando finish_reason é exatamente stop. length, content_filter, tool_calls, insufficient_system_resource, valor ausente ou valor desconhecido viram falha 502. Assim, o frontend não apresenta uma saída parcial como resposta completa.

4. Proxy do Vite no desenvolvimento

Substitua client/vite.config.js por:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      "/api": "http://localhost:3000",
    },
  },
});

O React chama uma URL relativa, e o Vite encaminha /api ao Express durante o desenvolvimento. Não é necessário liberar CORS para qualquer origem.

5. Frontend React

Substitua client/src/App.jsx:

import { useState } from "react";

function getSessionId() {
  const saved = sessionStorage.getItem("chat_session_id");
  if (saved) return saved;

  const created = crypto.randomUUID().replaceAll("-", "");
  sessionStorage.setItem("chat_session_id", created);
  return created;
}

export default function App() {
  const [sessionId] = useState(getSessionId);
  const [message, setMessage] = useState("");
  const [answer, setAnswer] = useState("");
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(false);

  async function handleSubmit(event) {
    event.preventDefault();
    const cleanMessage = message.trim();
    if (!cleanMessage || loading) return;

    setLoading(true);
    setAnswer("");
    setError("");

    try {
      const response = await fetch("/api/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message: cleanMessage, sessionId }),
      });

      const data = await response.json().catch(() => ({}));
      if (!response.ok) {
        throw new Error(data.error || "Falha ao gerar resposta.");
      }

      setAnswer(data.answer);
    } catch (requestError) {
      setError(requestError.message || "Não foi possível concluir a solicitação.");
    } finally {
      setLoading(false);
    }
  }

  return (
    <main className="app">
      <section className="card" aria-labelledby="app-title">
        <p className="eyebrow">Aplicativo independente</p>
        <h1 id="app-title">Chat com IA</h1>
        <p>
          A mensagem é enviada ao backend e processada por um fornecedor externo de IA.
          A resposta pode conter erros.
        </p>

        <form onSubmit={handleSubmit}>
          <label htmlFor="message">Sua pergunta</label>
          <textarea
            id="message"
            value={message}
            onChange={(event) => setMessage(event.target.value)}
            maxLength={4000}
            rows={6}
            required
          />
          <button type="submit" disabled={loading}>
            {loading ? "Gerando..." : "Enviar"}
          </button>
        </form>

        {error && <p className="error" role="alert">{error}</p>}

        {answer && (
          <section className="answer" aria-live="polite">
            <h2>Resposta gerada por IA</h2>
            <pre>{answer}</pre>
          </section>
        )}
      </section>
    </main>
  );
}

React escapa o texto interpolado por padrão. Não troque {answer} por dangerouslySetInnerHTML para renderizar Markdown ou HTML sem uma biblioteca e política de sanitização adequadas. A saída do modelo é conteúdo não confiável.

CSS mínimo

Substitua client/src/index.css:

:root {
  font-family: Inter, system-ui, sans-serif;
  color: #172033;
  background: #f3f6fb;
}

* { box-sizing: border-box; }
body { margin: 0; }
button, textarea { font: inherit; }

.app {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
}

.card {
  width: min(760px, 100%);
  padding: 32px;
  border: 1px solid #dce3ee;
  border-radius: 20px;
  background: #fff;
  box-shadow: 0 18px 50px rgba(23, 32, 51, 0.08);
}

.eyebrow { color: #3159d8; font-weight: 700; }
label { display: block; margin-bottom: 8px; font-weight: 700; }

textarea {
  width: 100%;
  padding: 14px;
  border: 1px solid #aebbd0;
  border-radius: 12px;
  resize: vertical;
}

button {
  margin-top: 12px;
  padding: 12px 18px;
  border: 0;
  border-radius: 10px;
  color: #fff;
  background: #3159d8;
  cursor: pointer;
}

button:disabled { opacity: 0.6; cursor: wait; }
.answer, .error { margin-top: 24px; }
.error { color: #a51d2d; }

.answer pre {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-family: inherit;
}

6. Executar e testar

Abra dois terminais na raiz. No primeiro:

npm run dev:server

No segundo:

npm run dev:client

Abra a URL mostrada pelo Vite. Para testar apenas o backend:

curl http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Explique a arquitetura deste aplicativo em três pontos.",
    "sessionId": "0123456789abcdef0123456789abcdef"
  }'

Para simular a implantação local:

npm run build
npm start

O Express servirá o diretório client/dist. Em uma plataforma de hospedagem, configure os segredos no painel do ambiente e mantenha HTTPS na borda. Não envie .env junto com os arquivos da aplicação.

Como ativar Thinking Mode

Thinking vem ativado por padrão na API V4. O tutorial desativa o recurso explicitamente para que o comportamento inicial não dependa de um default. Para testar raciocínio:

DEEPSEEK_MODEL=deepseek-v4-flash
DEEPSEEK_THINKING=enabled

O backend adiciona reasoning_effort: "high" quando Thinking está ativo. Os esforços efetivos documentados são high e max. Nesse modo, temperature, top_p, presence_penalty e frequency_penalty não alteram o resultado, mesmo que sejam aceitos por compatibilidade.

Este aplicativo envia somente uma pergunta por requisição e retorna apenas content. Se você adicionar histórico e ferramentas, precisa preservar corretamente a mensagem do assistente, incluindo reasoning_content quando houver Tool Calls. O tutorial específico de DeepSeek Tool Calls mostra esse loop.

O que user_id faz — e o que não faz

A documentação associa user_id a isolamento de segurança de conteúdo, KVCache e agendamento. O formato aceita letras, números, hífen e sublinhado, com até 512 caracteres, e não deve conter informação pessoal.

Neste exemplo, o navegador cria um UUID aleatório e o backend aplica HMAC. O valor resultante não é e-mail, nome ou ID direto do cliente. Mesmo assim, user_id não substitui login, autorização, controle de acesso ou isolamento de dados no seu banco. Um visitante ainda pode alterar o sessionId; a aplicação nunca deve usar esse campo para decidir a quais registros ele tem acesso.

Erros, retries e capacidade

StatusInterpretaçãoTratamento do aplicativo
400Formato inválidoCorrigir o payload; não repetir igual
401Falha de autenticaçãoVerificar ou rotacionar a chave no servidor
402Saldo insuficienteVerificar billing; não expor isso ao visitante
422Parâmetro inválidoCorrigir modelo, tipo ou valor
429Limite de capacidadeFila e backoff com jitter
500Erro do servidorRetry limitado e monitoramento
503SobrecargaRetry limitado, fallback e mensagem segura

Os limites documentados são de concorrência por conta: 2.500 para V4 Flash e 500 para V4 Pro. Uma requisição conta do envio até a conclusão da resposta, e várias chaves da mesma conta não multiplicam a capacidade. Esses números não são RPM nem TPM.

Adicione retry apenas para falhas transitórias e limite o número de tentativas. Em operações que criam efeitos externos, use idempotency key e não repita automaticamente uma ação cujo resultado seja desconhecido. O chat deste tutorial não executa Tool Calls.

Privacidade e responsabilidade do operador

A política de privacidade do serviço de consumo da DeepSeek não passa automaticamente a reger os usuários do seu aplicativo. Nos sistemas downstream, o desenvolvedor é responsável por suas regras de tratamento e deve informar os usuários, ter base legal quando necessária e responder aos direitos aplicáveis.

  • explique que a entrada é processada por IA e por um fornecedor externo;
  • informe finalidades, categorias de dados, destinatários e retenção;
  • minimize prompts e bloqueie segredos ou dados sensíveis desnecessários;
  • não grave mensagens completas em logs por padrão;
  • forneça exclusão e acesso quando exigidos;
  • não prometa zero retention ou ausência de treinamento sem compromisso verificável;
  • identifique saídas como geradas por IA e avise que podem conter erros.

Alinhe a implementação à sua política de privacidade e use a página de segurança para documentar controles e canal de reporte. A interface não deve sugerir que seu produto é oficial, autorizado ou recomendado pela DeepSeek.

Controle de custos sem misturar responsabilidades

  • aplique autenticação, quota por conta e limite diário;
  • comece com V4 Flash e teste Pro somente em tarefas definidas;
  • defina max_tokens proporcional ao formato esperado;
  • não reenvie histórico ilimitado;
  • monitore usage, latência, erros e custo por tarefa aceita;
  • crie alertas de saldo e um interruptor para suspender chamadas.

Os preços podem mudar. Consulte a fonte oficial antes de calcular orçamento e nunca mantenha um valor antigo codificado na interface.

Checklist antes do deploy

  • API key no secret manager e rotação documentada;
  • HTTPS, headers de segurança e dependências atualizadas;
  • autenticação e autorização para recursos não públicos;
  • validação de corpo, tipo e tamanho;
  • rate limit, quota, timeout e fila;
  • logs sem prompts, segredos ou dados pessoais por padrão;
  • política de retenção e mecanismo de exclusão;
  • saída renderizada como texto não confiável;
  • monitoramento de custo, latência, 429, 500 e 503;
  • testes de carga, abuso, falha do provedor e português brasileiro;
  • aviso de conteúdo gerado por IA e revisão humana em decisões críticas.

Perguntas frequentes

Posso colocar a DeepSeek API key no React?

Não. Qualquer segredo entregue ao navegador pode ser copiado. A chamada ao provedor deve sair do backend, e a chave deve ficar em variável protegida ou secret manager.

Qual ID devo usar em um aplicativo novo?

Use deepseek-v4-flash ou deepseek-v4-pro. Não use os aliases antigos em código novo.

deepseek-v4-pro substitui deepseek-reasoner?

Não como correspondência direta. Durante a transição oficial, deepseek-reasoner aponta para V4 Flash com Thinking. Pro é uma opção de upgrade que precisa de avaliação própria.

O aplicativo guarda o histórico?

Não nesta implementação. Cada envio contém somente a mensagem atual. Se você adicionar histórico, defina limite, retenção, acesso, exclusão e concatenação correta das mensagens.

user_id autentica o visitante?

Não. O campo auxilia no isolamento do lado da API, mas não prova identidade e não deve conceder acesso a dados.

Conclusão

Um aplicativo com a DeepSeek API precisa de mais do que uma chamada HTTP. A arquitetura deve manter a chave no servidor, limitar o que o frontend pode enviar, tratar respostas como não confiáveis e dar ao operador controle sobre custo, privacidade e indisponibilidade. O código desta página fornece esse esqueleto com React, Express, modelos V4 explícitos e comportamento de Thinking definido.

Antes de atender usuários reais, adicione autenticação, quotas, observabilidade, política de dados e testes adequados ao risco. Para executar funções externas, não altere este endpoint improvisando acesso a banco ou shell: implemente um loop controlado seguindo o guia de Tool Calls.

Fontes oficiais

Este conteúdo é independente e educacional. A DeepSeek não revisou, autorizou ou endossou este aplicativo ou tutorial.