---
title: Building a basic NextJS app with the Anam SDK
tags: [javascript, nextjs, beginner]
date: 2025-01-17
authors: [ao-anam]
---

The Anam JavaScript SDK lets you add conversational AI personas to web apps. By default, it captures audio from the user's microphone and streams back video of a talking avatar. In this cookbook we'll build a simple Next.js app that does exactly this.

The complete code is at [examples/basic-nextjs](https://github.com/anam-org/anam-cookbook/tree/main/examples/basic-nextjs).

## What you'll build

A single-page Next.js app with a video player. Users click to start, then talk into their microphone. The persona responds with voice and video.

## Prerequisites

- Node.js 18+
- An Anam account ([sign up at lab.anam.ai](https://lab.anam.ai))
- Your API key from the Anam Lab dashboard

## Project setup

Create a Next.js app and install the SDK:

```bash
pnpm create next-app@latest my-anam-app
cd my-anam-app
pnpm add @anam-ai/js-sdk
```

Create an `.env.local` file for your API key:

```bash
ANAM_API_KEY=your_api_key_here
```

<Tip>
Never expose your API key in client-side code. We'll generate session tokens from a server-side API route.
</Tip>

## Persona configuration

The persona config defines how your avatar looks and behaves. Put this in `src/config/persona.ts`:

```typescript
// src/config/persona.ts

export const personaConfig = {
  // Avatar - the visual character
  avatarId: "edf6fdcb-acab-44b8-b974-ded72665ee26",

  // Voice - how the persona sounds
  voiceId: "6bfbe25a-979d-40f3-a92b-5394170af54b",

  // LLM - the AI model powering conversations
  llmId: "0934d97d-0c3a-4f33-91b0-5e136a0ef466",

  // System prompt - defines personality and behavior
  systemPrompt: `You are a friendly AI assistant. Keep your responses concise and conversational.`,
};
```

You can browse avatars and voices at [lab.anam.ai](https://lab.anam.ai) and swap in different IDs.

## Session token API route

The Anam client needs a session token. Generate it server-side so your API key stays secret:

```typescript
// src/app/api/session-token/route.ts

import { NextResponse } from "next/server";
import { personaConfig } from "@/config/persona";

export async function POST() {
  const apiKey = process.env.ANAM_API_KEY;

  if (!apiKey) {
    return NextResponse.json(
      { error: "ANAM_API_KEY is not configured" },
      { status: 500 }
    );
  }

  try {
    const response = await fetch("https://api.anam.ai/v1/auth/session-token", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${apiKey}`,
      },
      body: JSON.stringify({ personaConfig }),
    });

    if (!response.ok) {
      const error = await response.text();
      console.error("Anam API error:", error);
      return NextResponse.json(
        { error: "Failed to get session token" },
        { status: response.status }
      );
    }

    const data = await response.json();
    return NextResponse.json({ sessionToken: data.sessionToken });
  } catch (error) {
    console.error("Error fetching session token:", error);
    return NextResponse.json(
      { error: "Failed to get session token" },
      { status: 500 }
    );
  }
}
```

## Building the persona player component

This component handles the video stream and connection lifecycle. We'll build it in pieces.

Start with imports:

```typescript
// src/components/PersonaPlayer.tsx

"use client";

import { useEffect, useRef, useState, useCallback } from "react";
import {
  createClient,
  AnamEvent,
  ConnectionClosedCode,
} from "@anam-ai/js-sdk";
import type { AnamClient, Message } from "@anam-ai/js-sdk";

type ConnectionState = "idle" | "connecting" | "connected" | "error";
```

### Fetching a session token

A wrapper to get tokens from our API route:

```typescript
async function fetchSessionToken(): Promise<string> {
  const response = await fetch("/api/session-token", { method: "POST" });
  if (!response.ok) {
    const data = await response.json();
    throw new Error(data.error || "Failed to get session token");
  }
  const { sessionToken } = await response.json();
  return sessionToken;
}
```

### Setting up event listeners

The SDK emits events for connection changes and new messages:

- `AnamEvent.CONNECTION_ESTABLISHED` - connected and ready
- `AnamEvent.MESSAGE_HISTORY_UPDATED` - new message in the conversation
- `AnamEvent.CONNECTION_CLOSED` - connection ended, with a reason code

```typescript
function setupEventListeners(
  client: AnamClient,
  handlers: {
    onConnected: () => void;
    onDisconnected: () => void;
    onError: (message: string) => void;
    onMessagesUpdated: (messages: Message[]) => void;
  }
) {
  client.addListener(AnamEvent.CONNECTION_ESTABLISHED, handlers.onConnected);

  client.addListener(AnamEvent.MESSAGE_HISTORY_UPDATED, handlers.onMessagesUpdated);

  client.addListener(AnamEvent.CONNECTION_CLOSED, (reason, details) => {
    if (reason !== ConnectionClosedCode.NORMAL) {
      handlers.onError(details || `Connection closed: ${reason}`);
    } else {
      handlers.onDisconnected();
    }
  });
}
```

### The component and session management

The component tracks connection state, errors, and messages. A ref holds the client instance for cleanup:

```typescript
export function PersonaPlayer() {
  const [connectionState, setConnectionState] = useState<ConnectionState>("idle");
  const [error, setError] = useState<string | null>(null);
  const [messages, setMessages] = useState<Message[]>([]);
  const clientRef = useRef<AnamClient | null>(null);

  const startSession = useCallback(async () => {
    setConnectionState("connecting");
    setError(null);

    try {
      const sessionToken = await fetchSessionToken();
      const client = createClient(sessionToken);
      clientRef.current = client;

      setupEventListeners(client, {
        onConnected: () => setConnectionState("connected"),
        onDisconnected: () => setConnectionState("idle"),
        onError: (message) => {
          setError(message);
          setConnectionState("error");
        },
        onMessagesUpdated: setMessages,
      });

      await client.streamToVideoElement("avatar-video");
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to start session");
      setConnectionState("error");
    }
  }, []);

  const stopSession = useCallback(() => {
    if (clientRef.current) {
      clientRef.current.stopStreaming();
      clientRef.current = null;
    }
    setConnectionState("idle");
    setMessages([]);
  }, []);

  // Clean up on unmount
  useEffect(() => {
    return () => {
      if (clientRef.current) {
        clientRef.current.stopStreaming();
      }
    };
  }, []);
```

`startSession` fetches a token, creates the client, sets up listeners, and calls `streamToVideoElement()` to start streaming. That handles all the WebRTC setup. Once it resolves, the persona greets the user and the microphone is active.

### Rendering the video player

The video element ID must match what we passed to `streamToVideoElement()`. The 3:2 aspect ratio matches the 720x480 stream:

```typescript
  return (
    <div className="flex flex-col gap-6 w-full max-w-4xl mx-auto">
      <div className="relative aspect-[3/2] bg-black rounded-lg overflow-hidden">
        <video
          id="avatar-video"
          autoPlay
          playsInline
          className="w-full h-full object-cover"
        />
      </div>
    </div>
  );
}
```

Add UI controls for each connection state:

```typescript
        {connectionState === "idle" && (
          <div className="absolute inset-0 flex items-center justify-center bg-gray-900">
            <button
              onClick={startSession}
              className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
            >
              Start conversation
            </button>
          </div>
        )}

        {connectionState === "connecting" && (
          <div className="absolute inset-0 flex items-center justify-center bg-gray-900">
            <div className="text-white">Connecting...</div>
          </div>
        )}

        {connectionState === "error" && (
          <div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-900 gap-4">
            <div className="text-red-400">{error}</div>
            <button
              onClick={startSession}
              className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
            >
              Try again
            </button>
          </div>
        )}

        {connectionState === "connected" && (
          <button
            onClick={stopSession}
            className="absolute top-4 right-4 px-3 py-1.5 bg-red-600 text-white text-sm rounded hover:bg-red-700 transition-colors"
          >
            End session
          </button>
        )}
```

Add a conversation history panel showing transcribed speech and responses:

```typescript
      {connectionState === "connected" && (
        <div className="h-48 overflow-y-auto bg-white rounded-lg border p-4 space-y-3">
          {messages.length === 0 ? (
            <p className="text-gray-500 text-sm">
              Start speaking to have a conversation...
            </p>
          ) : (
            messages.map((msg) => (
              <div
                key={msg.id}
                className={`text-sm ${
                  msg.role === "user" ? "text-blue-700" : "text-gray-800"
                }`}
              >
                <span className="font-medium">
                  {msg.role === "user" ? "You" : "Persona"}:
                </span>{" "}
                {msg.content}
              </div>
            ))
          )}
        </div>
      )}
```

## Adding the component to the page

Import and render the component:

```typescript
// src/app/page.tsx

import { PersonaPlayer } from "@/components/PersonaPlayer";

export default function Home() {
  return (
    <main>
      <h1>Anam Persona Demo</h1>
      <p>Click to start a conversation. Speak using your microphone.</p>
      <PersonaPlayer />
    </main>
  );
}
```

## Running the app

```bash
pnpm dev
```

Open [http://localhost:3000](http://localhost:3000), click "Start conversation", and talk to your persona.

Try editing `src/config/persona.ts` to change the system prompt. Swap in different avatar and voice IDs from [Anam Lab](https://lab.anam.ai) to see how they affect the experience.
