Topic #56Advanced6 min read

AI Feature Integration

Build AI features into the app — categorizers, chatbots, summarizers — always routing LLM calls through your backend.

#ai#llm#streaming#security#backend-for-frontend

The second way AI touches your work is the features you build into the app: chatbots, summarizers, smart categorization. From the frontend's perspective these are normal data interactions — you send some text to an endpoint and render the result — but with two twists: the call must never expose a provider key, and responses are often best streamed token by token.

A simple feature like an expense categorizer is just a POST to your own backend with the text to classify; the backend calls the LLM and returns a category. The frontend never touches the model or its key directly. This keeps the secret server-side and lets the backend apply guardrails (rate limits, prompt construction, input validation, content filtering) in one trusted place.

Chatbot-style features feel much faster when you stream the response instead of waiting for the full answer. The frontend reads the response body as a stream via the Streams API: get a reader from response.body, decode each chunk with a TextDecoder, and append it to state as tokens arrive. The UI updates incrementally — setResponse(prev => prev + chunk) — so the user sees text appear progressively, the same experience as a typing assistant.

The security pattern is the headline rule: never call an LLM provider directly from the browser with a secret key, because anything in the bundle is public. Route every AI call through your backend, which holds the key and enforces guardrails. This is true whichever provider you use — for example, calling Anthropic's Claude (such as claude-opus-4-8) belongs on the server, with the frontend talking only to your own /api/ai/* endpoints.

Backend Analogy

This is the classic backend-for-frontend / API gateway pattern. Just as you'd never let a browser hit your payment provider with the secret API key directly — you proxy through a server that holds the credential and validates the request — LLM calls go through your backend. The streaming response maps to server-sent events or a chunked HTTP response your Java/Vert.x layer would forward from the model.

Key Insights
  • Never expose an LLM provider key in the browser — proxy all AI calls through your backend, which holds the key and applies guardrails.
  • For an AI feature, the frontend just POSTs text to your own endpoint and renders the result; the model lives behind the backend.
  • Stream chatbot-style responses with the Streams API (reader + TextDecoder) and append chunks to state so text appears progressively.
  • Backend ownership of the call enables rate limiting, input validation, prompt construction, and content filtering in one trusted place.

Worked Code

Building an AI feature: expense categorizer
TypeScript
// Building an AI feature: expense categorizer
async function categorizeExpense(description: string): Promise<string> {
  // ALWAYS proxy through your backend — never expose API keys
  const response = await api.post('/api/ai/categorize', {
    description,
  });
  return response.data.category; // 'meals' | 'travel' | 'other'
}
Streaming AI responses (for chatbot-like features)
TypeScript
// Streaming AI responses (for chatbot-like features)
async function streamAIResponse(
  question: string,
  onChunk: (text: string) => void
) {
  const response = await fetch('/api/ai/ask', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ question }),
  });

  const reader = response.body!.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    onChunk(decoder.decode(value));
  }
}
Usage in component (append tokens as they arrive)
TSX
// Usage in component
function AIChatPanel() {
  const [response, setResponse] = useState('');

  const ask = async (question: string) => {
    setResponse('');
    await streamAIResponse(question, (chunk) => {
      setResponse(prev => prev + chunk); // append as tokens arrive
    });
  };

  return <div className="whitespace-pre-wrap">{response}</div>;
}

Interview-Ready Q&A

From the frontend, you treat it as a normal request to your own backend: POST the input text to an endpoint like /api/ai/categorize and render the returned result. The backend is what actually calls the model (for example Anthropic's Claude, claude-opus-4-8) using the secret key it holds. This keeps the key off the client, and it lets the backend build the prompt, validate input, rate-limit, and filter output. The frontend stays thin — it never imports a provider SDK or knows the model name.

Things to Remember
  • 1Golden rule: never call an LLM provider from the browser with a secret key — proxy through your backend.
  • 2An AI feature on the frontend is just a POST to your own /api/ai/* endpoint that returns the result.
  • 3Stream responses with response.body.getReader() + TextDecoder, appending chunks to state for progressive output.

References & Further Reading