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-proe o experimentaldeepseek-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 blocoimage_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/chatno 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_idpseudô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
| Camada | Responsabilidade | Contém a API key? |
|---|---|---|
| Navegador / React | Coletar mensagem, mostrar estado e renderizar texto | Não |
| Backend / Express | Autenticar, validar, limitar, chamar o provedor e filtrar erros | Sim, via ambiente ou secret manager |
| DeepSeek API | Processar a entrada e gerar a saída | Recebe a credencial no header Bearer |
| Banco opcional | Contas, consentimentos e histórico com retenção definida | Nã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
| Necessidade | Configuração inicial | Observação |
|---|---|---|
| Chat rápido e frequente | deepseek-v4-flash com Thinking desativado | Bom ponto de partida para medir latência e custo |
| Raciocínio com menor custo relativo | deepseek-v4-flash com Thinking ativado | Foi o mapeamento documentado durante a transição do antigo deepseek-reasoner |
| Tarefa complexa após avaliação | deepseek-v4-pro | Upgrade opcional; não é o substituto direto do alias antigo |
| Imagem, screenshot, gráfico ou OCR | deepseek-v4-flash-vision-exp | Exige 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
fetcheAbortSignal.timeoutnativos; - 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
| Status | Interpretação | Tratamento do aplicativo |
|---|---|---|
| 400 | Formato inválido | Corrigir o payload; não repetir igual |
| 401 | Falha de autenticação | Verificar ou rotacionar a chave no servidor |
| 402 | Saldo insuficiente | Verificar billing; não expor isso ao visitante |
| 422 | Parâmetro inválido | Corrigir modelo, tipo ou valor |
| 429 | Limite de capacidade | Fila e backoff com jitter |
| 500 | Erro do servidor | Retry limitado e monitoramento |
| 503 | Sobrecarga | Retry 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_tokensproporcional 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
- Release oficial do DeepSeek V4 Preview e retirada dos aliases
- Modelos, recursos e preços da API
- Referência oficial de Chat Completions
- Guia oficial do Thinking Mode
- Guia oficial de Vision
- Files API para imagens reutilizáveis
- Concorrência e isolamento por user_id
- Termos da DeepSeek Open Platform
Este conteúdo é independente e educacional. A DeepSeek não revisou, autorizou ou endossou este aplicativo ou tutorial.
