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.

Baseline autenticado para o backend do aplicativo

Validamos o passo mínimo de backend: uma requisição HTTPS autenticada a /chat/completions, com a chave mantida somente em memória do ambiente de teste. V4 Flash retornou HTTP 200, finish_reason=stop e uso de 31 tokens de entrada, 24 de saída e 55 no total.

Escopo: não publicamos nem executamos um aplicativo completo, frontend, banco de dados, login, rate limiting ou deploy nesta auditoria. O teste comprova apenas o baseline da API. Nunca envie a chave ao navegador; mantenha-a no servidor ou em um gerenciador de segredos.

Veja o fluxo prático de Chat Completions, a auditoria da API e o tratamento de erros.

Em 29/07/2026, os aliases legados responderam HTTP 200 e retornaram V4 Flash nesta conta, embora ausentes de GET /models. Isso não garante continuidade e descreve somente a data do teste. O catálogo atual está documentado no aviso abaixo.

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 documental: 23 de agosto de 2026. O teste autenticado acima permanece datado de 29 de julho de 2026. Este é um guia independente, sem afiliação, autorização ou endosso da DeepSeek.

Atualização documental — 23 de agosto de 2026. O catálogo atual inclui deepseek-v4-flash, deepseek-v4-pro e o experimental deepseek-v4-flash-vision-exp. O allowlist abaixo aceita os três IDs, mas este tutorial implementa deliberadamente uma interface somente de texto: não há campo de upload nem bloco image_url. Para um aplicativo visual, crie uma rota separada, valide imagem, tamanho, origem, autorização e retenção, e use Vision Exp. Essa exclusão é uma decisão de escopo do tutorial, não uma limitação geral da API.

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 três IDs hospedados atuais;
  • 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 ativadoFoi o mapeamento documentado durante a transição do antigo deepseek-reasoner
Tarefa complexa após avaliaçãodeepseek-v4-proUpgrade opcional; não é o substituto direto do alias antigo
Imagem, screenshot, gráfico ou OCRdeepseek-v4-flash-vision-expExige payload visual e validações que este tutorial textual não implementa

Comece com uma configuração explícita e promova tarefas específicas ao Pro somente depois de medir qualidade, latência e custo. Use Vision Exp apenas quando a tarefa depender de imagem; selecioná-lo sem enviar imagem não transforma a interface React em um upload visual. 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 DEEPSEEK_REASONING_EFFORT=high 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", "deepseek-v4-flash-vision-exp"]) .default("deepseek-v4-flash"), DEEPSEEK_THINKING: z.enum(["enabled", "disabled"]).default("disabled"), DEEPSEEK_REASONING_EFFORT: z.enum(["low", "high", "max"]).default("high"), 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 = env.DEEPSEEK_REASONING_EFFORT; } 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 os três IDs hospedados atuais; o endpoint continua textual por decisão de escopo;
  • 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 DEEPSEEK_REASONING_EFFORT=low

O backend envia o reasoning_effort escolhido em DEEPSEEK_REASONING_EFFORT quando Thinking está ativo. Os esforços documentados são low, high e max; o padrão é high, e medium e xhigh são mapeados para high. Configure DEEPSEEK_REASONING_EFFORT no ambiente em vez de aceitar um valor enviado pelo navegador. 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, não inclui o parâmetro tools e retorna apenas content; por isso, não precisa reenviar reasoning_content. Se você adicionar histórico e enviar tools com Thinking ativado, preserve a mensagem completa do assistente — inclusive reasoning_content — em todas as requisições subsequentes, mesmo nos turnos em que o modelo não gerar 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 para este chat textual. O allowlist também aceita deepseek-v4-flash-vision-exp, mas o tutorial não envia imagens por decisão de escopo; implemente e valide um payload visual separado antes de usá-lo para Vision. Não use aliases antigos em código novo.

deepseek-v4-pro substitui deepseek-reasoner?

Não como correspondência direta. Durante a janela de compatibilidade oficial, deepseek-reasoner apontava 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.