---
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
```
Never expose your API key in client-side code. We'll generate session tokens from a server-side API route.
## 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 {
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("idle");
const [error, setError] = useState(null);
const [messages, setMessages] = useState([]);
const clientRef = useRef(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 (
);
}
```
Add UI controls for each connection state:
```typescript
{connectionState === "idle" && (
)}
{connectionState === "connecting" && (
Connecting...
)}
{connectionState === "error" && (
{error}
)}
{connectionState === "connected" && (
)}
```
Add a conversation history panel showing transcribed speech and responses:
```typescript
{connectionState === "connected" && (
)}
```
## 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 (
Anam Persona Demo
Click to start a conversation. Speak using your microphone.
);
}
```
## 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.