
English summary: Combines RSC with AI streaming to keep tokens server-side.
Você quer integrar LLMs no React, mas:
React Server Components (RSC) rodam no servidor, permitindo chamar LLMs sem expor credenciais.
// ❌ NUNCA faça isso (expõe API key)
'use client';
import OpenAI from 'openai';
export default function Chat() {
const openai = new OpenAI({
apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY // ❌ Exposto no cliente!
});
async function sendMessage(message: string) {
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: message }]
});
return response.choices[0].message.content;
}
}
// app/chat/page.tsx (Server Component por padrão)
import { OpenAI } from 'openai';
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY // ✅ Server-side only
});
export default async function ChatPage() {
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello!' }]
});
return (
<div>
<h1>AI Response</h1>
<p>{response.choices[0].message.content}</p>
</div>
);
}
Problema: Não é interativo. Precisa de Server Actions.
// app/actions.ts (Server Action)
'use server';
import { OpenAI } from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function generateResponse(message: string) {
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: message }]
});
return response.choices[0].message.content;
}
// app/chat/page.tsx (Client Component)
'use client';
import { useState } from 'react';
import { generateResponse } from '../actions';
export default function ChatPage() {
const [message, setMessage] = useState('');
const [response, setResponse] = useState('');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const aiResponse = await generateResponse(message); // Chama server
setResponse(aiResponse);
}
return (
<form onSubmit={handleSubmit}>
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Ask AI..."
/>
<button type="submit">Send</button>
{response && <p>{response}</p>}
</form>
);
}
npm install ai
// app/actions.ts
'use server';
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { createStreamableValue } from 'ai/rsc';
export async function generateStreamingResponse(message: string) {
const stream = createStreamableValue('');
(async () => {
const { textStream } = await streamText({
model: openai('gpt-4'),
messages: [{ role: 'user', content: message }]
});
for await (const delta of textStream) {
stream.update(delta);
}
stream.done();
})();
return { output: stream.value };
}
'use client';
import { useState } from 'react';
import { readStreamableValue } from 'ai/rsc';
import { generateStreamingResponse } from '../actions';
export default function StreamingChat() {
const [message, setMessage] = useState('');
const [generation, setGeneration] = useState('');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setGeneration('');
const { output } = await generateStreamingResponse(message);
for await (const delta of readStreamableValue(output)) {
setGeneration((current) => current + delta);
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Ask AI..."
/>
<button type="submit">Send</button>
<div>{generation}</div> {/* Atualiza palavra por palavra */}
</form>
);
}
// app/actions.ts
'use server';
import { embed, embedMany } from 'ai';
import { openai } from '@ai-sdk/openai';
import { db } from '@/lib/db'; // Prisma/Drizzle
export async function ragQuery(question: string) {
// 1. Embed question
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: question
});
// 2. Vector search (PostgreSQL + pgvector)
const docs = await db.$queryRaw`
SELECT content
FROM documents
ORDER BY embedding <=> ${embedding}::vector
LIMIT 3
`;
const context = docs.map(d => d.content).join('\n\n');
// 3. Generate with context
const stream = createStreamableValue('');
(async () => {
const { textStream } = await streamText({
model: openai('gpt-4'),
messages: [
{
role: 'system',
content: `Answer based on this context:\n${context}`
},
{ role: 'user', content: question }
]
});
for await (const delta of textStream) {
stream.update(delta);
}
stream.done();
})();
return { output: stream.value, sources: docs };
}
// app/actions.ts
'use server';
import { streamText, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
export async function aiWithTools(message: string) {
const result = await streamText({
model: openai('gpt-4'),
messages: [{ role: 'user', content: message }],
tools: {
weather: tool({
description: 'Get weather for a location',
parameters: z.object({
location: z.string().describe('City name')
}),
execute: async ({ location }) => {
// Call real weather API
const response = await fetch(
`https://api.weather.com/${location}`
);
return response.json();
}
}),
calculate: tool({
description: 'Perform calculations',
parameters: z.object({
expression: z.string()
}),
execute: async ({ expression }) => {
return eval(expression); // Cuidado: sanitize em produção
}
})
}
});
return result.toAIStreamResponse();
}
// app/chat/page.tsx
'use client';
import { useChat } from 'ai/react';
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: '/api/chat' // Route handler que usa aiWithTools
});
return (
<div>
{messages.map(m => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button type="submit">Send</button>
</form>
</div>
);
}
'use server';
import { cache } from 'react';
import { openai } from '@ai-sdk/openai';
// Cache per request (deduplication)
export const getCachedEmbedding = cache(async (text: string) => {
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: text
});
return embedding;
});
// Uso
const embedding1 = await getCachedEmbedding('hello'); // API call
const embedding2 = await getCachedEmbedding('hello'); // Cached (mesma request)
'use server';
import { headers } from 'next/headers';
import { rateLimiter } from '@/lib/rate-limit';
export async function generateResponse(message: string) {
const ip = headers().get('x-forwarded-for') || 'unknown';
const allowed = await rateLimiter.isAllowed(ip);
if (!allowed) {
throw new Error('Rate limit exceeded');
}
// ... gerar resposta
}
'use server';
import { createStreamableUI } from 'ai/rsc';
export async function generateUI(message: string) {
const ui = createStreamableUI();
(async () => {
ui.update(<div>Thinking...</div>);
const response = await llm.generate(message);
ui.done(
<div>
<h2>Result</h2>
<p>{response}</p>
</div>
);
})();
return ui.value;
}
'use client';
export default function Page() {
const [ui, setUI] = useState(null);
async function handleClick() {
const result = await generateUI('Hello');
setUI(result);
}
return (
<div>
<button onClick={handleClick}>Generate</button>
{ui}
</div>
);
}
React Server Components não são apenas sobre performance. São sobre segurança (API keys no servidor) e capabilities (RAG, tool use, streaming).
Se você está fazendo LLM calls no cliente, você está expondo credenciais.
Cenário: Portal B2B com recomendações. Portal moveu prompts e fetches para server components; cliente só recebe UI pronta com suspense boundaries.
| Métrica | Antes | Depois |
|---|---|---|
| TTFB | 1.9 s | 620 ms |
| Bundle JS | 950 KB | 340 KB |
| Churn por UX lenta | 12% | 4% |
Produtos gratuitos e pagos para transformar ideias em uma base que você consegue executar.
13 produtos disponíveisContinue explorando tópicos similares

Em 2025, eu trabalhei 14 horas por dia durante 3 meses seguidos. Por FOMO. O resultado? Insonia. Irritabilidade. Zero criatividade. É a ironia: minha produtividade CAIU. Esse artigo de 65 minutos tem 20+ pesquisas, 7 pilares e um plano de 12 semanas para sair do burnout de forma sustentável.

App Router e Pages Router parecem uma discussão de moda, mas a diferença real está em modelo mental, limites entre servidor e cliente, cache, streaming, SEO, DX e custo de migração. Este guia mostra quando migrar, quando ficar e como evitar uma escolha cara demais para o seu produto.

Guia robusto para aplicar Dependency Inversion Principle com ports and adapters, React Context, contract tests e observabilidade de adapters em produção.
Checklist de 47 pontos para encontrar bugs, riscos de segurança e problemas de performance antes do lançamento.
Templates testados em produção, usados por desenvolvedores. Economize semanas de setup no seu próximo projeto.