← Blog

Claude API Integration with Vue.js: A Setup Guide

2026-09-26 · 5 min read · SubToAPI Team

Integrating Claude into a Vue.js app comes down to two decisions: where the API calls happen (never the browser, always a backend) and how you structure the request/response flow so your components stay reactive without extra boilerplate. This guide walks through both, with a working Vue 3 composable you can drop into a project today.

The short answer for anyone in a hurry: you cannot call api.anthropic.com directly from a Vue frontend because it requires a secret API key that would be exposed to every visitor. You need a thin backend (Node, a serverless function, or a proxy like SubToAPI) that holds the key, and your Vue app talks to that backend over plain HTTPS. Below is the full setup, from architecture to streaming UI updates.

Why You Can't Call Claude Directly from Vue

Vue apps ship as JavaScript bundles that run in the browser. Any API key embedded in that bundle — even minified or in an environment variable prefixed with VITE_ — is visible to anyone who opens dev tools. Anthropic's API keys are meant to stay server-side, so a direct fetch from a Vue component will either fail on CORS or leak your credentials.

The standard pattern is:

  1. Vue frontend sends a request to your own backend endpoint (e.g. /api/chat).
  2. Backend attaches the real API key and forwards the request to Claude.
  3. Backend streams or returns the response back to Vue.

This is true whether you're calling Anthropic directly or a proxy service. If you already have a Node/Express or Nuxt server, this is a few lines of code. If you don't want to run a backend at all, a service like SubToAPI gives you an HTTPS endpoint your Vue app can call with a scoped sub_live_... key — the key still shouldn't sit in frontend code, but you can put it behind a lightweight edge function or even a serverless proxy with minimal setup. Either way, the Vue-side integration pattern below stays identical.

Minimal Backend Proxy

A basic Express route that forwards chat requests:

app.post('/api/chat', async (req, res) => {
  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,
      messages: req.body.messages,
    }),
  });
  const data = await response.json();
  res.json(data);
});

This uses SubToAPI as the upstream so you get one dashboard for keys, usage, and team seats regardless of which framework consumes it — see /docs/quickstart for the full request format.

A Vue Composable for Chat

Wrap the fetch logic in a composable so any component can use it without duplicating state management:

// composables/useClaudeChat.js
import { ref } from 'vue'

export function useClaudeChat() {
  const messages = ref([])
  const loading = ref(false)
  const error = ref(null)

  async function sendMessage(text) {
    messages.value.push({ role: 'user', content: text })
    loading.value = true
    error.value = null

    try {
      const res = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ messages: messages.value }),
      })
      const data = await res.json()
      messages.value.push({ role: 'assistant', content: data.content[0].text })
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  return { messages, loading, error, sendMessage }
}

Then in a component:

<script setup>
import { ref } from 'vue'
import { useClaudeChat } from '@/composables/useClaudeChat'

const { messages, loading, sendMessage } = useClaudeChat()
const input = ref('')

function submit() {
  if (!input.value.trim()) return
  sendMessage(input.value)
  input.value = ''
}
</script>

<template>
  <div v-for="(m, i) in messages" :key="i" :class="m.role">
    {{ m.content }}
  </div>
  <input v-model="input" @keyup.enter="submit" :disabled="loading" />
</template>

This gives you a reactive chat interface with no extra state library. messages is a plain ref array, so Vue's reactivity handles re-renders automatically.

Handling Streaming Responses

Chat UIs feel much better when text appears token by token instead of waiting for the full response. Claude's API supports server-sent events for this, and you can consume them in Vue using the Fetch API's readable stream reader:

async function sendMessageStreamed(text) {
  messages.value.push({ role: 'user', content: text })
  const assistantMsg = { role: 'assistant', content: '' }
  messages.value.push(assistantMsg)

  const res = await fetch('/api/chat-stream', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ messages: messages.value.slice(0, -1) }),
  })

  const reader = res.body.getReader()
  const decoder = new TextDecoder()

  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    const chunk = decoder.decode(value)
    assistantMsg.content += chunk
  }
}

Because assistantMsg is a reference held inside the reactive messages array, mutating .content directly triggers Vue's reactivity and updates the DOM incrementally. Your backend proxy needs to pass through the streamed chunks from Claude's stream: true responses — see /docs/streaming for the event format if you're building the proxy yourself, or use SubToAPI's streaming endpoint which handles the SSE parsing on the upstream side.

Managing Conversation State and Tool Use

For anything beyond a single-turn chat, keep the full message history in a ref and pass it back on every request — Claude's API is stateless, so context comes entirely from the messages array you send. If you're building an assistant that calls functions (looking up orders, querying a database, hitting an internal API), the request/response shape includes tool_use blocks that you handle before sending a follow-up message with the tool result. That flow is documented at /docs/tools and works the same way regardless of frontend framework — Vue just needs to render the intermediate "thinking" or "calling tool" state if you want to show it to users.

Getting Started Without Building the Backend Yourself

If the backend proxy is the part you'd rather skip, SubToAPI gives you an API key (sub_live_...) tied to your existing Claude access, with streaming, tool use, and usage metadata already handled. Your Vue app calls a single HTTPS endpoint instead of maintaining a separate Anthropic integration. Plans start at €9/month for solo use, with team seats at €19 and €49/seat for scale — see /pricing. You can start with a free trial at /signup and follow /docs/quickstart to get your first request working in a few minutes.

questions

Can I call the Claude API directly from a Vue.js frontend without a backend? No. API keys must stay server-side. You need at minimum a serverless function or lightweight proxy between your Vue app and Claude's API.

How do I show streaming responses in a Vue component? Read the response body as a stream with the Fetch API's getReader(), decode chunks, and append them to a reactive ref's content field — Vue re-renders automatically as the value changes.

Does this integration pattern work the same in Nuxt? Yes. Nuxt's server routes (server/api/) can act as the backend proxy directly, so you don't need a separate Express server — the Vue-side composable code is unchanged.

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 →