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-flashoudeepseek-v4-pro. Os aliasesdeepseek-chatedeepseek-reasonerserã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/chatno 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_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 | É a correspondência da 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 |
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
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
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-flashoudeepseek-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
| 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. 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
- 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
- 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.
