← Blog

Using Neureus with Next.js App Router

A complete guide to integrating Neureus AI into a Next.js 15 App Router project — streaming chat, RAG, and server actions. Production patterns, not toy examples.

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

Try Neureus AI — start free

500 Neurons/month, no credit card required. The complete AI application backend in one API.