← Blog

How to Build a Multi Turn Conversation App

2026-10-31 · 5 min read · SubToAPI Team

A multi-turn conversation app is any application where the model remembers what was said earlier in the session — a chatbot, a coding assistant, a support agent — rather than treating each request as a one-shot prompt. The core technique is simple: you don't send just the latest user message, you send the entire conversation history (or a trimmed version of it) with every request, and the model uses that array to generate a contextually aware reply.

This article walks through the actual mechanics: how message arrays work, how to manage growing context, how to handle streaming replies in a chat UI, and where things commonly break (duplicate roles, lost system prompts, runaway token counts). The examples use the Claude Messages API format, which is also what SubToAPI exposes, so the same code works whether you're calling Anthropic directly or through a proxy.

The core data structure: a messages array

Every multi-turn app is built around one data structure: an ordered list of {role, content} objects, alternating between user and assistant. You own this array — the API itself is stateless and has no memory between calls. Each request you send must include everything the model needs to know.

[
  { "role": "user", "content": "I'm building a recipe app. What stack would you pick?" },
  { "role": "assistant", "content": "For a recipe app I'd lean toward..." },
  { "role": "user", "content": "What about image storage specifically?" }
]

On every turn, you append the new user message, send the full array, append the assistant's reply to your local copy once it comes back, and repeat. This is the entirety of "memory" in a multi-turn app — there's no hidden server-side state unless your platform explicitly adds one.

Minimal working example

const messages = [];

async function sendTurn(userText) {
  messages.push({ role: "user", content: userText });

  const res = await fetch("https://api.subtoapi.app/v1/messages", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      model: "claude-sonnet-4-5",
      max_tokens: 1024,
      system: "You are a concise, pragmatic engineering advisor.",
      messages
    })
  });

  const data = await res.json();
  const reply = data.content[0].text;
  messages.push({ role: "assistant", content: reply });
  return reply;
}

Call sendTurn() for each new user input and the messages array accumulates the conversation. This is the skeleton every multi-turn app is built on — a web chat, a Slack bot, a CLI tool, a voice assistant transcript — the storage and transport differ, but the array pattern doesn't change. Full request/response shapes are in /docs/messages.

Where state actually lives

In the example above, state lives in a JavaScript variable, which disappears on refresh or restart. For a real app you need to persist it:

Whichever you choose, the server-side call pattern is identical — load the array, append the new message, send it, append the reply, save it back.

Managing context growth

Conversations get long, and context windows aren't infinite. Left unmanaged, a multi-turn app will eventually hit token limits or get noticeably slower and more expensive per turn. A few practical strategies:

  1. Sliding window: keep only the last N turns and drop older ones. Simple, loses long-term context.
  2. Summarize-and-replace: periodically ask the model to summarize the conversation so far, replace the older messages with that summary as a single system or user message, and continue from there.
  3. Selective retrieval: store the full history externally (a database) and only inject the turns relevant to the current question, using embeddings or keyword search.

Most production apps combine 1 and 2: keep the last handful of turns verbatim for conversational flow, and compress everything older into a running summary. This keeps token usage predictable and avoids the classic bug where a chat app silently fails after turn 40 because the request exceeds the model's context window.

Streaming replies for a responsive UI

Multi-turn apps feel sluggish if users stare at a blank screen waiting for a full response. Streaming fixes this by returning tokens as they're generated:

const res = await fetch("https://api.subtoapi.app/v1/messages", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    model: "claude-sonnet-4-5",
    max_tokens: 1024,
    messages,
    stream: true
  })
});

for await (const chunk of res.body) {
  // append chunk text to the UI as it arrives
}

Buffer the full streamed text locally and only push it into your messages array once the stream completes — appending partial chunks mid-stream will corrupt the next request's history. See /docs/streaming for event format details.

Common mistakes

If you're prototyping this without wanting to manage API key infrastructure, provisioning, and billing yourself, SubToAPI turns your Claude access into a key you can drop straight into the code above — sign up at /signup, check /pricing, or start with /docs/quickstart.

questions

Do I need a special "conversation" or "session" API to build this? No. The Messages API is stateless — you just resend the full message array each turn. Sessions are a concept you build on your own side (database row, session ID), not something the API provides.

How many turns can a conversation have before it breaks? There's no hard turn limit, but there is a token limit per request (the context window). Long conversations need trimming or summarization once their total token count approaches that limit.

Should I store conversation history client-side or server-side? Server-side is safer for anything beyond a toy project — it lets you enforce access control, avoid tampered histories, and recover sessions across devices. Client-side storage is fine for quick demos.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →