Next.js App Router introduced server components and server actions as the default way to handle data fetching and mutations. Neureus integrates naturally with both patterns — server components for displaying AI-generated content, server actions for mutations, and route handlers for streaming.
This guide covers the three patterns you’ll actually need in production.
Prerequisites
npm install @neureus/sdk
Set your API key in .env.local:
NEUREUS_API_KEY=nr_your_key_here
Create a singleton client — important to avoid creating a new instance per request:
// lib/neureus.ts
import { NeureuAI } from '@neureus/sdk';
export const neureus = new NeureuAI({
apiKey: process.env.NEUREUS_API_KEY!,
});
Pattern 1: Server Components for AI-generated content
Server components run on the server and can call external APIs directly. For non-streaming AI content (summaries, classifications, extracted data), server components are the right pattern.
// app/products/[id]/page.tsx
import { neureus } from '@/lib/neureus';
import { getProduct } from '@/lib/db';
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
// Generate AI summary server-side — no client JS needed
const { text: summary } = await neureus.ai.chat({
model: '@wai/llama-3.3-70b', // free, fast
messages: [
{
role: 'system',
content: 'Summarize this product in 2-3 sentences for a marketing page. Focus on key benefits.',
},
{
role: 'user',
content: `Product: ${product.name}\nDescription: ${product.description}\nSpecs: ${JSON.stringify(product.specs)}`,
},
],
});
return (
<main>
<h1>{product.name}</h1>
<p className="ai-summary">{summary}</p>
{/* rest of product page */}
</main>
);
}
What makes this work well: The summary is generated at request time (or cached) without any client JavaScript. Users get fast, SEO-indexable content.
When to cache: If the product data doesn’t change often, wrap in unstable_cache or use revalidate:
// Cache for 1 hour, revalidate when product changes
export const revalidate = 3600;
Pattern 2: Route Handlers for streaming
For interactive chat interfaces, you need streaming. App Router route handlers return ReadableStream directly.
// app/api/chat/route.ts
import { NextRequest } from 'next/server';
import { neureus } from '@/lib/neureus';
export const runtime = 'edge'; // Neureus runs on CF edge too — keep latency low
export async function POST(req: NextRequest) {
const { messages, useRag, query } = await req.json();
// Optional: RAG retrieval before generation
let systemContext = '';
if (useRag && query) {
const { results } = await neureus.rag.query({ query, topK: 5 });
if (results.length > 0) {
systemContext = `\n\nRelevant context from your knowledge base:\n${results
.map(r => r.content)
.join('\n\n---\n\n')}`;
}
}
const stream = await neureus.ai.stream({
model: 'claude-sonnet-4-6',
messages: [
{
role: 'system',
content: `You are a helpful assistant.${systemContext}`,
},
...messages,
],
});
// Neureus returns a ReadableStream — pipe it through
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
On the client, consume the stream with the Vercel AI SDK’s useChat hook or read the SSE stream manually:
// components/ChatInterface.tsx
'use client';
import { useState } from 'react';
export function ChatInterface() {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [streaming, setStreaming] = useState('');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const userMessage = { role: 'user', content: input };
setMessages(prev => [...prev, userMessage]);
setInput('');
setStreaming('');
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: [...messages, userMessage] }),
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let fullText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// Parse SSE: "data: {text}\n\n"
for (const line of chunk.split('\n')) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.text) {
fullText += data.text;
setStreaming(fullText);
}
} catch {}
}
}
}
setMessages(prev => [...prev, { role: 'assistant', content: fullText }]);
setStreaming('');
}
return (
<div>
{messages.map((m, i) => <div key={i}>{m.content}</div>)}
{streaming && <div>{streaming}</div>}
<form onSubmit={handleSubmit}>
<input value={input} onChange={e => setInput(e.target.value)} />
<button type="submit">Send</button>
</form>
</div>
);
}
Pattern 3: Server Actions for mutations
Server actions are perfect for AI mutations that don’t need streaming — document ingestion, agent runs, batch submissions.
// app/documents/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { neureus } from '@/lib/neureus';
import { auth } from '@/lib/auth';
export async function ingestDocument(formData: FormData) {
const session = await auth();
if (!session) throw new Error('Unauthorized');
const url = formData.get('url') as string;
const title = formData.get('title') as string;
// Use the tenant's client (per-user API key or your shared key)
const tenantClient = new NeureuAI({
apiKey: session.user.neureus_api_key, // per-user key from your DB
});
const { documentId, chunks } = await tenantClient.rag.ingest({
url,
title,
chunkSize: 512,
overlap: 64,
});
// Revalidate the documents list
revalidatePath('/documents');
return { documentId, chunks };
}
// app/documents/page.tsx
import { ingestDocument } from './actions';
export default function DocumentsPage() {
return (
<form action={ingestDocument}>
<input name="url" placeholder="https://docs.yourproduct.com" />
<input name="title" placeholder="Document title" />
<button type="submit">Ingest document</button>
</form>
);
}
Multi-tenant patterns
If you’re building a SaaS where each user has their own Neureus API key (from your onboarding), create the client per-request:
// lib/neureus-tenant.ts
import { NeureuAI } from '@neureus/sdk';
import { auth } from '@/lib/auth';
export async function getTenantClient() {
const session = await auth();
if (!session?.user?.neureus_api_key) throw new Error('No Neureus key for this user');
return new NeureuAI({ apiKey: session.user.neureus_api_key });
}
Or let users provide their own OpenAI keys (BYOK):
// After user saves their OpenAI key in your settings UI:
const client = await getTenantClient();
await client.ai.setProviderKey('openai', userProvidedOpenAIKey);
// Future calls from this tenant automatically use their key
Error handling
The SDK throws NeureuAPIError on non-2xx responses. In Next.js server components, errors propagate to the nearest error.tsx:
// app/products/[id]/error.tsx
'use client';
export default function Error({ error }: { error: Error }) {
return <div>Failed to generate summary: {error.message}</div>;
}
In route handlers, catch explicitly:
import { NeureuAPIError } from '@neureus/sdk';
export async function POST(req: NextRequest) {
try {
// ... Neureus calls
} catch (err) {
if (err instanceof NeureuAPIError) {
return Response.json({ error: err.message }, { status: err.status });
}
return Response.json({ error: 'Internal error' }, { status: 500 });
}
}
What’s next
- RAG API — ingest documents and query them
- Batch Inference — async processing at 40% off
- AI Gateway — model routing and pricing
- TypeScript SDK reference