n4nAI

Build a generative UI chat with n4n.ai and RSC

Hands-on generative ui chat n4n.ai rsc tutorial: build a streaming RSC chat with Vercel AI SDK and the n4n.ai OpenAI-compatible endpoint for LLM apps.

n4n Team3 min read594 words

Audio narration

Coming soon — every post will get a voice note here.

This generative ui chat n4n.ai rsc tutorial shows how to pipe a streaming LLM response into React Server Components that render as tools execute. We’ll use the Vercel AI SDK’s RSC primitives with n4n.ai’s OpenAI-compatible endpoint, which addresses 240+ models and provides automatic fallback when a provider is degraded.

Prerequisites

  • Node.js 18.18 or later.
  • A Next.js 14 app using the App Router (RSC enabled by default).
  • An API key from n4n.ai exported as N4N_API_KEY in .env.local.
  • Basic comfort with TypeScript, React Server Components, and client/server boundaries.

Scaffold the project

Create a minimal Next.js app and add the AI SDK packages.

pnpm create next-app@latest rsc-chat --ts --app --no-tailwind --eslint
cd rsc-chat
pnpm add ai @ai-sdk/openai zod

The --no-tailwind flag keeps the example free of styling noise; we’ll use inline styles.

Define the server action

Create app/action.tsx. Mark the file with 'use server' so every export becomes a server action. We use streamUI from ai/rsc to handle the model stream and tool calls.

// app/action.tsx
'use server';
import { streamUI } from 'ai/rsc';
import { createOpenAI } from '@ai-sdk/openai';
import { z } from 'zod';
import { ReactNode } from 'react';

const n4n = createOpenAI({
  baseURL: 'https://api.n4n.ai/v1',
  apiKey: process.env.N4N_API_KEY,
});

async function fetchWeather(city: string) {
  // Stand-in for a real geo/weather API
  return { city, temp: 22, condition: 'Clear' };
}

function WeatherSkeleton() {
  return <div style={{ padding: 8, border: '1px solid #ccc' }}>Loading weather…</div>;
}

function WeatherCard({ city, temp, condition }: { city: string; temp: number; condition: string }) {
  return (
    <div style={{ padding: 8, border: '1px solid #0a0', borderRadius: 4 }}>
      <strong>{city}</strong>: {temp}°C, {condition}
    </div>
  );
}

export async function submitMessage(input: string): Promise<ReactNode> {
  const { value } = await streamUI({
    model: n4n.chat('gpt-4o-mini'),
    prompt: input,
    text: ({ content }) => <p>{content}</p>,
    tools: {
      getWeather: {
        parameters: z.object({ city: z.string() }),
        generate: async function* ({ city }) {
          yield <WeatherSkeleton />;
          const data = await fetchWeather(city);
          return <WeatherCard {...data} />;
        },
      },
    },
  });
  return value;
}

streamUI returns a value containing the final React tree. The text renderer handles plain tokens; the getWeather tool yields a skeleton immediately, then swaps in the card when data resolves. Because the endpoint is OpenAI-compatible, the createOpenAI helper works without custom adapters.

Build the client chat component

The client component manages input and appends each turn to the UI state.

// app/chat.tsx
'use client';
import { useState } from 'react';
import { useUIState } from 'ai/rsc';
import { submitMessage } from './action';

export default function Chat() {
  const [input, setInput] = useState('');
  const [uiState, setUIState] = useUIState();

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!input.trim()) return;
    const userMsg = <div key={`u-${uiState.length}`}>You: {input}</div>;
    setUIState((prev) => [...prev, userMsg]);
    const response = await submitMessage(input);
    setUIState((prev) => [...prev, <div key={`a-${uiState.length}`}>{response}</div>]);
    setInput('');
  }

  return (
    <div>
      <div style={{ display: 'grid', gap: 8, marginBottom: 12 }}>{uiState}</div>
      <form onSubmit={handleSubmit} style={{ display: 'flex', gap: 8 }}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          style={{ flex: 1, padding: 6 }}
          placeholder="Ask about weather…"
        />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

useUIState is a distributed state store; each call to setUIState merges new React nodes that stream from the server. The server action submitMessage is called directly because it carries the 'use server' directive.

Wire up the page

app/page.tsx renders the chat inside a centered container.

// app/page.tsx
import Chat from './chat';

export default function Page() {
  return (
    <main style={{ maxWidth: 600, margin: '2rem auto', fontFamily: 'sans-serif' }}>
      <h2>Generative UI Chat</h2>
      <Chat />
    </main>
  );
}

Run and verify

Start the dev server:

pnpm dev

Open http://localhost:3000. Type What's the weather in Oslo? and submit. You should see:

You: What's the weather in Oslo?
The weather in Oslo is currently:
[ Oslo: 22°C, Clear ]

The skeleton flashes before the card appears, proving the tool streamed a server component. If the model ignores the tool, you’ll just get a text response—adjust the prompt or model parameters. Check the browser network tab for ?_rsc requests carrying the serialized component payload.

How the RSC stream works

Under the hood, streamUI uses React’s server streaming transport. The client receives a serialized component tree, not HTML strings, so interactive islands (if any) hydrate normally. The text renderer streams incremental paragraphs; tool generate functions can yield multiple times, letting you show loading states without client-side fetch logic.

Handle multi-turn and state

The example above is stateless per request. For real chat, persist history with getMutableAIState inside the action:

import { getMutableAIState } from 'ai/rsc';

export async function submitMessage(input: string) {
  const state = getMutableAIState();
  state.update([...state.get(), { role: 'user', content: input }]);
  const { value } = await streamUI({
    model: n4n.chat('gpt-4o-mini'),
    messages: state.get(),
    text: ({ content }) => <p>{content}</p>,
    tools: {
      getWeather: {
        parameters: z.object({ city: z.string() }),
        generate: async function* ({ city }) {
          yield <WeatherSkeleton />;
          const data = await fetchWeather(city);
          return <WeatherCard {...data} />;
        },
      },
    },
  });
  state.done();
  return value;
}

This keeps the conversation coherent across renders and lets the model reference earlier turns when deciding to call tools.

Routing and fallback notes

If you need to force a specific provider, n4n.ai honors client routing directives sent in the request headers, so you can pin a model per call without changing code. The gateway also forwards provider cache-control hints, letting you reuse cached prompt prefixes where supported. That matters when you chain multiple tool calls in one turn and want to avoid re-paying for static system prompts.

Debugging tips

  • Confirm N4N_API_KEY is loaded: add console.log(process.env.N4N_API_KEY?.slice(0,4)) in the action temporarily.
  • If the client throws Cannot read properties of undefined, ensure useUIState is called inside a 'use client' component.
  • Tool parameters must be a Zod schema; plain TS types will silently fail at runtime.

Extending with more tools

Add another tool the same way:

tools: {
  getWeather: { /* … */ },
  getTime: {
    parameters: z.object({ tz: z.string() }),
    generate: async function* ({ tz }) {
      yield <div>Loading time…</div>;
      return <div>{new Date().toLocaleTimeString('en-US', { timeZone: tz })}</div>;
    },
  },
}

The model will pick the right tool based on the prompt. Because n4n.ai fronts many providers, a rate-limit on one backend won’t break the stream—the gateway shifts to a healthy route.

Production checks

  • Server actions must be async and marked 'use server'.
  • streamUI tools must return React nodes, not strings.
  • Use useUIState on the client; never import server components directly into client files.
  • Deploy on a platform that supports App Router streaming (Vercel, Node 18+ server).

That’s the full loop: a generative UI chat built on RSC with a single OpenAI-compatible endpoint.

Tagsgenerative-uin4n-airscchat

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All vercel ai sdk generative ui with react server components posts →