← Blog

How to Build a Claude-Powered Writing Assistant

2026-11-01 · 5 min read · SubToAPI Team

A Claude-powered writing assistant is an app that sends user text and instructions to Claude's API and returns edited, rewritten, or generated copy in real time. Building one means three things: designing prompts that reliably produce the writing behavior you want (tone shifts, grammar fixes, expansion, summarization), streaming the response so users see words appear as they're generated, and wiring up an API layer that handles authentication, rate limits, and usage tracking without you writing that infrastructure from scratch.

This guide walks through the actual implementation — from prompt structure to a working streaming endpoint — so you can ship a writing assistant feature instead of just prototyping one in a playground.

What a writing assistant actually needs to do

Most writing assistant features fall into a few repeatable modes:

Each mode needs a different system prompt and, often, different temperature settings. Treating "writing assistant" as one monolithic prompt leads to inconsistent output — the model tries to guess what you want instead of being told explicitly.

Step 1: Design mode-specific system prompts

Instead of one generic prompt, define a small set of system prompts keyed by mode:

const MODES = {
  rewrite: "You are an editing assistant. Rewrite the user's text in the requested tone. Preserve all factual content and intent. Return only the rewritten text, no commentary.",
  proofread: "You are a proofreading assistant. Fix grammar, spelling, and punctuation errors only. Do not change the writer's voice, word choices, or structure unless they are incorrect. Return only the corrected text.",
  expand: "You are a writing assistant that expands short text into fuller prose while keeping the original meaning and tone. Return only the expanded text.",
  continue: "You are a writing assistant. Continue the user's text naturally, matching their tone and style. Write 1-3 sentences. Return only the continuation, no repetition of the original."
};

The instruction "return only the X, no commentary" matters more than it looks — without it, Claude will often wrap output in explanations ("Here's your rewritten text:") that you then have to strip out in post-processing.

Step 2: Build the request with user-adjustable parameters

Tone and length are usually user-facing controls, not hardcoded. Pass them as part of the user message rather than baking them into the system prompt:

async function editText(mode, text, options = {}) {
  const { tone = "neutral", maxLength } = options;

  const userContent = maxLength
    ? `Tone: ${tone}. Max length: ${maxLength} words.\n\nText:\n${text}`
    : `Tone: ${tone}.\n\nText:\n${text}`;

  const response = 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: MODES[mode],
      messages: [{ role: "user", content: userContent }]
    })
  });

  const data = await response.json();
  return data.content[0].text;
}

This keeps the system prompt stable (good for caching and predictability) while letting the UI expose tone sliders or dropdowns without touching backend logic.

Step 3: Stream the output to the UI

Writing assistants feel instant when text streams word by word instead of appearing all at once after a multi-second wait. Server-Sent Events are the standard approach:

async function streamEdit(mode, text, onToken) {
  const response = 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: MODES[mode],
      stream: true,
      messages: [{ role: "user", content: text }]
    })
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const chunk = decoder.decode(value);
    for (const line of chunk.split("\n")) {
      if (!line.startsWith("data:")) continue;
      const payload = line.slice(5).trim();
      if (payload === "[DONE]") return;
      const parsed = JSON.parse(payload);
      if (parsed.type === "content_block_delta") {
        onToken(parsed.delta.text);
      }
    }
  }
}

onToken appends each piece directly to a text area or contenteditable div in the UI. Full streaming event details, including delta types and error events, are documented at /docs/streaming.

Step 4: Preserve conversation context for multi-turn editing

A good writing assistant lets users say "make it shorter" after a rewrite, without re-pasting the text. That requires keeping the conversation history and appending turns:

let history = [{ role: "user", content: originalText }];

async function refine(instruction) {
  history.push({ role: "user", content: instruction });
  const response = 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: MODES.rewrite,
      messages: history
    })
  });
  const data = await response.json();
  const reply = data.content[0].text;
  history.push({ role: "assistant", content: reply });
  return reply;
}

Trim history once it grows past a few turns — most writing assistant interactions don't need more than the last 2-3 exchanges for context, and shorter context keeps latency and token cost down.

Step 5: Handle rate limits and usage from day one

If you're shipping this as a product feature, you'll eventually need per-user usage limits, team seats, and a clean way to issue and revoke API keys without building that billing/auth layer yourself. SubToAPI wraps Claude access in application API keys (sub_live_...), gives you usage metadata per request, and handles streaming and tool use out of the box — so the writing assistant logic above works against it with no changes beyond the endpoint and key. Setup takes a few minutes; see /docs/quickstart for the full flow, or start with a free trial at /signup. Plans start at €9/month for solo projects, scaling to team and usage-based tiers on /pricing.

Testing output quality before shipping

Before launch, run each mode against edge cases: empty input, text that's already well-written, very long paragraphs, and text in a different language than your default. Proofread mode in particular needs testing against text with intentional stylistic quirks (slang, fragments) — a model that "corrects" style instead of just grammar will frustrate users who wanted their voice preserved.

Questions

Does the model need fine-tuning to act as a writing assistant? No. System prompts with explicit mode instructions and output constraints (like "return only the rewritten text") are sufficient for nearly all writing assistant use cases. Fine-tuning adds cost and maintenance for marginal gains here.

Should I use streaming for short edits like proofreading a sentence? Streaming is less noticeable for short outputs, but it's still worth enabling consistently across modes — it simplifies your frontend code and avoids a jarring difference in perceived speed between short and long edits.

How do I stop Claude from adding explanations before the edited text? Add an explicit instruction in the system prompt such as "return only the result, no preamble or commentary." This is the single most effective fix for unwanted wrapper text in writing assistant outputs.

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 →