Full Stack AI: React + FastAPI + LangChain em Produção
Full Stack AI: React + FastAPI + LangChain em Produção
Full Stack AIFeatured

Full Stack AI: React + FastAPI + LangChain em Produção

Arquitetura completa de uma aplicação de IA com streaming SSE, autenticação, banco vetorial e deploy. O que aprendi construindo a MSc Academy.

M
Moises Costa
January 25, 202620 min read
ReactFastAPILangChainFull StackSSEDeploy

Full Stack AI: React + FastAPI + LangChain em Produção

Construir uma aplicação de IA completa — do frontend ao deploy — envolve muito mais do que integrar uma API de LLM. Neste artigo, compartilho a arquitetura que uso na MSc Academy e as decisões de design que fiz ao longo do caminho.

A Stack Completa

Frontend: React 19 + Tailwind CSS 4 + Framer Motion
Backend: Node.js + Express + tRPC (MSc Academy)
         Python + FastAPI (serviços de IA)
LLM: LangChain + LangGraph + Claude/GPT-4
DB: MySQL/TiDB + PGvector (Supabase)
Deploy: Manus (frontend+backend) + Docker

Streaming SSE: A Chave para UX de IA

A experiência de ver o texto aparecer palavra por palavra é fundamental para aplicações de IA. Implemento isso com Server-Sent Events (SSE):

Backend (Node.js/Express)

typescript
app.get("/api/chat/stream", async (req, res) => {
  // Configura SSE
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");
  res.setHeader("Access-Control-Allow-Origin", "*");

  const { message, sessionId } = req.query;
  
  try {
    // Stream do LLM token por token
    const stream = await llm.stream([
      { role: "system", content: SYSTEM_PROMPT },
      ...await getHistory(sessionId),
      { role: "user", content: message },
    ]);

    for await (const chunk of stream) {
      const token = chunk.choices[0]?.delta?.content || "";
      if (token) {
        res.write(`data: ${JSON.stringify({ token })}\n\n`);
      }
    }
    
    res.write("data: [DONE]\n\n");
  } catch (error) {
    res.write(`data: ${JSON.stringify({ error: "Erro interno" })}\n\n`);
  } finally {
    res.end();
  }
});

Frontend (React)

typescript
function useStreamingChat() {
  const [streamingText, setStreamingText] = useState("");
  const [isStreaming, setIsStreaming] = useState(false);

  const sendMessage = useCallback(async (message: string) => {
    setIsStreaming(true);
    setStreamingText("");
    
    const eventSource = new EventSource(
      `/api/chat/stream?message=${encodeURIComponent(message)}&sessionId=${sessionId}`
    );

    eventSource.onmessage = (event) => {
      if (event.data === "[DONE]") {
        eventSource.close();
        setIsStreaming(false);
        return;
      }
      
      const { token } = JSON.parse(event.data);
      setStreamingText(prev => prev + token);
    };

    eventSource.onerror = () => {
      eventSource.close();
      setIsStreaming(false);
    };
  }, [sessionId]);

  return { streamingText, isStreaming, sendMessage };
}

Autenticação Simples sem OAuth

Para a MSc Academy, optei por autenticação simples com JWT em vez de OAuth para simplificar o onboarding:

typescript
// Usuários hardcoded para demo + registro de novos usuários
const DEMO_USERS = {
  admin: { password: "admin", role: "admin" },
  Moises: { password: "admin", role: "admin" },
};

app.post("/api/auth/login", async (req, res) => {
  const { username, password } = req.body;
  
  // Verifica usuários demo
  const demoUser = DEMO_USERS[username];
  if (demoUser && demoUser.password === password) {
    const token = await createJWT({ username, role: demoUser.role });
    res.cookie("session", token, { httpOnly: true, maxAge: 365 * 24 * 60 * 60 * 1000 });
    return res.json({ success: true });
  }
  
  // Verifica usuários registrados no banco
  const dbUser = await db.getUserByUsername(username);
  if (dbUser && await bcrypt.compare(password, dbUser.passwordHash)) {
    const token = await createJWT({ username, role: dbUser.role });
    res.cookie("session", token, { httpOnly: true });
    return res.json({ success: true });
  }
  
  return res.status(401).json({ error: "Credenciais inválidas" });
});

tRPC: Type Safety End-to-End

Uma das melhores decisões que tomei foi usar tRPC. Ele elimina toda a camada de serialização/deserialização:

typescript
// server/routers.ts
export const appRouter = router({
  topics: router({
    list: publicProcedure.query(async () => {
      return await db.getAllTopics();
    }),
    getBySlug: publicProcedure
      .input(z.object({ slug: z.string() }))
      .query(async ({ input }) => {
        return await db.getTopicBySlug(input.slug);
      }),
  }),
  
  exercises: router({
    verify: protectedProcedure
      .input(z.object({
        exerciseId: z.number(),
        answer: z.string(),
      }))
      .mutation(async ({ input, ctx }) => {
        const exercise = await db.getExercise(input.exerciseId);
        const isCorrect = checkAnswer(exercise, input.answer);
        
        if (isCorrect) {
          await db.recordProgress(ctx.user.id, input.exerciseId);
        }
        
        return { isCorrect, explanation: exercise.explanation };
      }),
  }),
});

// client/src/pages/Exercises.tsx
const { data: exercises } = trpc.exercises.list.useQuery({ topicId });
const verifyMutation = trpc.exercises.verify.useMutation({
  onSuccess: (data) => {
    if (data.isCorrect) toast.success("Correto! 🎉");
    else toast.error(`Incorreto. ${data.explanation}`);
  },
});

Arquitetura de Componentes de IA

Para o avatar do Arquimedes, criei um sistema de estados visuais que responde ao estado da conversa:

typescript
type AvatarState = "idle" | "thinking" | "speaking" | "celebrating";

function ArquimedesAvatar({ state }: { state: AvatarState }) {
  const animations = {
    idle: { y: [0, -8, 0], transition: { repeat: Infinity, duration: 3 } },
    thinking: { rotate: [-2, 2, -2], transition: { repeat: Infinity, duration: 0.5 } },
    speaking: { scale: [1, 1.02, 1], transition: { repeat: Infinity, duration: 0.3 } },
    celebrating: { y: [0, -20, 0], rotate: [0, 10, -10, 0], transition: { duration: 0.6 } },
  };

  return (
    <motion.div animate={animations[state]}>
      <img src="/manus-storage/Arquimedes_f63227f7.webp" alt="Arquimedes" />
    </motion.div>
  );
}

Deploy e Performance

Para o deploy da MSc Academy, uso a plataforma Manus que oferece:

  • Frontend + Backend integrados: sem CORS, sem configuração de proxy
  • MySQL gerenciado: sem preocupação com backups ou manutenção
  • CDN automático: assets servidos via CloudFront
  • SSL automático: HTTPS sem configuração

Para otimização de performance:

typescript
// Lazy loading de páginas pesadas
const Chat = lazy(() => import("./pages/Chat"));
const Topics = lazy(() => import("./pages/Topics"));

// Prefetch de dados críticos
queryClient.prefetchQuery({
  queryKey: ["topics"],
  queryFn: () => trpc.topics.list.query(),
});

Lições Aprendidas

1. Comece com SSE, não WebSockets: SSE é mais simples, funciona com HTTP/2 e é suficiente para streaming unidirecional.

2. tRPC vale o investimento: a type safety end-to-end economiza horas de debugging.

3. Otimize o bundle desde o início: lazy loading e code splitting são muito mais difíceis de adicionar depois.

4. Monitore o custo de tokens: em produção, o custo de LLM pode surpreender. Implemente rate limiting e caching de respostas comuns.

5. UX de loading é tão importante quanto a resposta: spinners, skeletons e streaming text fazem a diferença na percepção de velocidade.

Conclusão

Construir aplicações Full Stack AI é um campo em rápida evolução. A stack que uso hoje — React + tRPC + LangChain + SSE — é o resultado de muitas iterações e erros. O mais importante é começar, iterar rápido e medir o impacto real no usuário.


Moises Costa é AI Engineer e criador da MSc Academy. Veja o código no GitHub.

Live AI Agents

See the concepts in action

Interact with Arquimedes, Atlas, Artemis and the Detran-RJ agent — built with the exact techniques described in this article.