n4nAI

Vercel Edge Functions and n4n.ai: a streaming chat walkthrough

Build a streaming chat API on Vercel Edge Functions using the n4n.ai OpenAI-compatible gateway. Step-by-step tutorial with runnable TypeScript code.

n4n Team3 min read620 words

Audio narration

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

This tutorial builds a vercel edge functions n4n.ai streaming chat endpoint that pipes tokens from an OpenAI-compatible gateway straight to the browser. We’ll stand up a single Edge route that handles streaming, error propagation, and request forwarding without a middle tier.

Prerequisites

  • Node.js 18+ and the Vercel CLI (npm i -g vercel)
  • A project scoped to the Edge runtime (Next.js App Router or a vanilla api/ directory)
  • An API key for the gateway, exported as N4N_API_KEY
  • Familiarity with TypeScript and Server-Sent Events (SSE)

If you’re new to Edge functions, know that they run on V8 isolates, not Node.js. That matters for streaming: the runtime stays resident and supports ReadableStream natively.

Scaffold the project

Create a minimal Next.js App Router layout. The only file that matters is the route. For a non-Next setup, drop the same code in api/chat.ts with a default export.

// app/api/chat/route.ts
export const runtime = 'edge';

const GATEWAY = 'https://api.n4n.ai/v1/chat/completions';

The gateway exposes an OpenAI-compatible /v1/chat/completions shape, so any existing OpenAI client code works if you point it at that URL.

Implement the streaming route

The route accepts a JSON body { messages: {role: string, content: string}[] }, forwards it to the gateway with stream: true, and returns the raw SSE stream after minimal validation.

export async function POST(req: Request): Promise<Response> {
  const { messages } = await req.json();
  if (!Array.isArray(messages)) {
    return new Response('bad request', { status: 400 });
  }

  const upstream = await fetch(GATEWAY, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      authorization: `Bearer ${process.env.N4N_API_KEY}`,
    },
    body: JSON.stringify({
      model: 'gpt-4o-mini',
      messages,
      stream: true,
    }),
  });

  if (!upstream.ok || !upstream.body) {
    return new Response('upstream error', { status: 502 });
  }

  // Pipe the upstream SSE directly to the client.
  return new Response(upstream.body, {
    headers: {
      'content-type': 'text/event-stream',
      'cache-control': 'no-cache, no-transform',
    },
  });
}

That’s the entire backend. The gateway fronts 240+ models and auto-falls back when a provider is degraded, so we don’t need retry logic in the Edge function. Per-token usage metering happens at the gateway, so your function never tallies tokens.

Expected output at the boundary

Run vercel dev and call the route with curl:

curl -N -X POST http://localhost:3000/api/chat \
  -H 'content-type: application/json' \
  -d '{"messages":[{"role":"user","content":"Say hi in 5 words"}]}'

You’ll see raw SSE frames identical to what the gateway emits:

data: {"choices":[{"delta":{"content":"Hello"},"finish_reason":null}]}
data: {"choices":[{"delta":{"content":" there"},"finish_reason":null}]}
data: {"choices":[{"delta":{"content":""},"finish_reason":"stop"}]}
data: [DONE]

The Edge function adds no parsing overhead; bytes move from the gateway to the client as they arrive. If you see a 502, check that N4N_API_KEY is set in the dev environment.

Add client-side rendering

A browser consumer can read the stream with fetch and getReader(). You don’t need a library to prove the path works.

const res = await fetch('/api/chat', {
  method: 'POST',
  body: JSON.stringify({ messages }),
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let text = '';
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  const chunk = decoder.decode(value);
  // chunk contains "data: {...}\n" lines; parse out delta.content
  for (const line of chunk.split('\n')) {
    if (line.startsWith('data: ') && !line.includes('[DONE]')) {
      const json = JSON.parse(line.slice(6));
      text += json.choices[0].delta.content ?? '';
    }
  }
}
console.log(text);

For production, use the OpenAI Node shim or an event-stream parser. The point is that the vercel edge functions n4n.ai streaming chat route is transport-agnostic—it neither knows nor cares how the client renders tokens.

Error handling and timeouts

Edge functions have a max duration (default 30s on Vercel). Streaming keeps the connection open, but you should guard against hangs upstream.

const controller = new AbortController();
const t = setTimeout(() => controller.abort(), 25_000);
const upstream = await fetch(GATEWAY, {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    authorization: `Bearer ${process.env.N4N_API_KEY}`,
  },
  body: JSON.stringify({ model: 'gpt-4o-mini', messages, stream: true }),
  signal: controller.signal,
});
clearTimeout(t);

If the abort fires, the gateway connection drops and the client stream closes cleanly. Because the gateway handles provider fallback, most transient errors never reach this layer. You still want the timeout so a stuck socket doesn’t burn the function’s full budget.

Routing and caching passthrough

The gateway honors client routing directives and forwards provider cache-control hints. In practice this means you can pass extra headers through the Edge function and they’ll reach the upstream provider unchanged. We treat them as pass-through:

headers: {
  'content-type': 'application/json',
  authorization: `Bearer ${process.env.N4N_API_KEY}`,
  // add routing/cache headers here as needed
},

No transformation required. The Edge function stays dumb on purpose.

Why Edge, not Node serverless

Edge runtime starts in milliseconds and keeps connections open without pinning a lambda instance. For token streaming, that means the client sees the first byte quickly and the function isn’t billed for idle wait between tokens. Standard Node serverless supports response streaming but cold starts add latency and concurrency limits bite under load.

For a streaming chat on Vercel Edge Functions, the isolate model fits: small code, no state, high connection count.

Minimal HTML test page

Drop this in public/test.html to manually exercise the route without curl:

<!doctype html>
<html>
<body>
<textarea id="in" placeholder="message"></textarea>
<button onclick="send()">Send</button>
<pre id="out"></pre>
<script>
async function send() {
  const res = await fetch('/api/chat', {
    method: 'POST',
    body: JSON.stringify({ messages: [{ role: 'user', content: document.getElementById('in').value }] })
  });
  const reader = res.body.getReader();
  const dec = new TextDecoder();
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    document.getElementById('out').textContent += dec.decode(value);
  }
}
</script>
</body>
</html>

Open the deployed URL’s /test.html, type a prompt, and watch tokens land.

Deploy

vercel deploy --prod
vercel env add N4N_API_KEY

The Edge function replicates to all regions. Users get low-latency streaming because the function runs close to them and the gateway connection is opened from the edge node.

Keep the route minimal. If you need auth, add it before the fetch call. If you need model switching, read it from the request body and pass it through. The streaming plumbing doesn’t change.

Tagsverceledge-functionsn4n-aistreaming

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 edge functions llm streaming posts →