AI agents by framework / JavaScript and TypeScript

How to add an AI agent to a Nuxt app

An AI agent in a Nuxt app is a Nitro server route that calls a model with tools, runs the tool calls the model makes against your own server utilities as the signed-in user, and streams the result to a Vue component. The AI SDK and its Vue bindings cover the model loop and the chat UI; you supply the tools, authorization and limits. If the agent only needs to do what your pages already do, you can embed one instead.

What an agent in a Nuxt app is made of

  • A server endpoint: a Nitro server route such as server/api/chat.post.ts that holds the model API key and calls the model. Model keys stay in private runtimeConfig or server environment variables, never in runtimeConfig.public.
  • Tools: typed functions the model may call, each wrapping an operation your app already has, typically a function in server/utils that queries your database or calls an internal API.
  • The agent loop: the model calls a tool, receives the result and decides the next step until it can answer. Cap the number of steps.
  • A chat UI: a Vue component that streams messages from the server route and shows tool activity.
  • Authorization and approval: the server route reads the user from your session (for example with nuxt-auth-utils), tools act as that user, and actions that change data wait for the user to approve them.

Ways to add an agent to Nuxt

ApproachGood forWhat you take on
AI SDK in a Nitro server routeA custom agent with your own tools, model provider and Vue chat UI.Tool definitions, authorization in every tool, approval UI, rate and cost limits.
An agent framework (for example Mastra or LangGraph.js)Long-running or multi-agent workflows with memory and orchestration.Another layer to learn and deploy, plus everything in the row above.
Hosted answer-only chat widgetQuestions about your docs or help center.Keeping its content current. It does not act in your app.
Embedded in-app agent (Bunkhouse)Letting signed-in users get tasks done through the pages and API you already have.A script tag, an identity endpoint and hints for operations discovery cannot see.

Build an agent with the AI SDK

The AI SDK is Vercel's TypeScript library for model calls, tool calling and chat UIs; @ai-sdk/vue provides its Vue bindings. The examples below use AI SDK 7 with Nuxt 4 and nuxt-auth-utils for the session. Install them with Zod for tool input schemas:

Terminal
npm install ai @ai-sdk/vue zod

The model string "anthropic/claude-sonnet-5.5" goes through the Vercel AI Gateway, which reads the API key from the AI_GATEWAY_API_KEY server environment variable. To call a provider directly, install its package (for example @ai-sdk/openai) and pass its model instead. Add toolApprovalSecret: "" to runtimeConfig in nuxt.config.ts and set it with NUXT_TOOL_APPROVAL_SECRET; the route below reads it.

1. The server route

Files in server/api/ become API routes, and the .post.ts suffix limits this one to POST /api/chat; see the Nuxt server directory. The handler calls requireUserSession first, which answers 401 when nobody is signed in. Each tool closes over that user, so the model can choose an order ID but never whose orders it reads or cancels. The condition isStepCount(5) lets the model call tools and continue for up to five steps. The setting toolApproval makes cancelOrder wait for the user, and experimental_toolApprovalSecret signs approval requests so a client cannot forge one in the message history it sends back. The handler returns a web Response, which Nitro streams to the browser as is. The abort controller stops the model when the connection closes before the stream has finished, for example when the user leaves the page.

server/api/chat.post.ts
import {
  convertToModelMessages,
  createUIMessageStreamResponse,
  isStepCount,
  streamText,
  toUIMessageStream,
  tool,
  type UIMessage,
} from "ai";
import { z } from "zod";
import { cancelOrder, listOrders } from "../utils/orders"; // your existing data layer

export default defineEventHandler(async (event) => {
  // nuxt-auth-utils: throws 401 when there is no session
  const { user } = await requireUserSession(event);
  const { messages } = await readBody<{ messages: UIMessage[] }>(event);
  const config = useRuntimeConfig(event);
  // Stop the model if the client disconnects before the stream finishes.
  const controller = new AbortController();
  event.node.res.on("close", () => {
    if (!event.node.res.writableFinished) controller.abort();
  });

  const result = streamText({
    model: "anthropic/claude-sonnet-5.5",
    instructions: "You help the signed-in user manage their orders. Use the tools; never guess order data.",
    messages: await convertToModelMessages(messages),
    tools: {
      listOrders: tool({
        description: "List the signed-in user's recent orders",
        inputSchema: z.object({ status: z.enum(["open", "shipped", "cancelled"]).optional() }),
        execute: async ({ status }) => listOrders(user.id, { status }),
      }),
      cancelOrder: tool({
        description: "Cancel one of the signed-in user's open orders",
        inputSchema: z.object({ orderId: z.string() }),
        execute: async ({ orderId }) => cancelOrder(user.id, orderId),
      }),
    },
    toolApproval: { cancelOrder: "user-approval" },
    experimental_toolApprovalSecret: config.toolApprovalSecret,
    stopWhen: isStepCount(5),
    abortSignal: controller.signal,
  });

  return createUIMessageStreamResponse({
    stream: toUIMessageStream({ stream: result.stream }),
  });
});

2. The chat component

The useChat composable from @ai-sdk/vue posts to /api/chat by default and keeps the streamed reply in a reactive messages ref. When the model calls cancelOrder, the tool part arrives in the state approval-requested. The user's answer is sent with addToolApprovalResponse, and the option sendAutomaticallyWhen resumes the conversation once every pending approval has a response.

app/components/AgentChat.vue
<script setup lang="ts">
import { useChat } from "@ai-sdk/vue";
import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
import { ref } from "vue";

const input = ref("");
const { messages, sendMessage, addToolApprovalResponse } = useChat({
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
});

function send() {
  sendMessage({ text: input.value });
  input.value = "";
}
<\/script>

<template>
  <div v-for="message in messages" :key="message.id">
    <template v-for="(part, i) in message.parts" :key="i">
      <p v-if="part.type === 'text'">{{ part.text }}</p>
      <div
        v-else-if="part.type === 'tool-cancelOrder' && part.state === 'approval-requested' && !part.approval.isAutomatic"
      >
        <p>Cancel order {{ JSON.stringify(part.input) }}?</p>
        <button @click="addToolApprovalResponse({ id: part.approval.id, approved: true })">Approve</button>
        <button @click="addToolApprovalResponse({ id: part.approval.id, approved: false })">Deny</button>
      </div>
    </template>
  </div>
  <form @submit.prevent="send">
    <input v-model="input" aria-label="Message" />
  </form>
</template>

Production checklist for Nuxt

  • Authorize in the route and in every tool. Route middleware in app/middleware only guards page navigation; server routes must check the session themselves with requireUserSession or your auth library's server helper, and every query must be scoped to that user. See Sessions and authentication.
  • Add CSRF protection. Nuxt has none built in. The session cookie from nuxt-auth-utils is SameSite=Lax, which stops most cross-site POSTs; for defense in depth, reject requests whose Sec-Fetch-Site is not same-origin, or use a module such as nuxt-csurf and send its token from useChat through a transport with headers.
  • Treat tool input and tool results as untrusted. The model chooses tool arguments, and data a tool returns (user-written text, emails, web pages) can contain instructions. Validate arguments with the schema and your own business rules, and require approval for anything irreversible.
  • Limit cost. Cap steps, rate-limit /api/chat per user (for example in a Nitro server middleware) and set spending limits with your model provider.
  • Allow for long responses. The Node server output streams for as long as the connection stays open; on serverless and edge presets, raise the function timeout for the chat route and check that any proxy in front of Nitro does not buffer the response.
  • Keep secrets server-side. The model key and approval secret belong in server environment variables or private runtimeConfig, never in runtimeConfig.public, which is sent to the browser.

Option: embed an in-app agent with Bunkhouse

If the agent's job is to help signed-in users do work your Nuxt app already supports, you may not need to build the tools, agent loop and chat UI yourself. Bunkhouse adds an Assistant pane to an existing web app with one script tag. When a user opens it, the assistant works with what the page already offers: buttons, links, form fields and tables, the API requests your app makes, a hints file you publish for operations it cannot see, and WebMCP tools your page declares.

The model writes small programs that run in a sandbox on the embed origin, not in your page. They can only call capabilities the assistant has discovered and your configuration allows. Write actions are shown to the user for review before they run, the model never sees credentials, and your Nuxt backend keeps enforcing its own authorization on every request.

In Nuxt, render the loader from app.vue with useHead on the server only and tagPriority: "critical", so it sits at the top of the server-rendered head, ahead of Nuxt's own scripts, and client-side navigation never adds it again. For signed-in users, add a server/api/agent-identity.get.ts route that signs a short-lived token for the user your session already authenticated; the signing secret stays in a server environment variable.

app/app.vue (excerpt)
// app/app.vue, <script setup lang="ts"> (excerpt)
if (import.meta.server) {
  useHead(
    {
      script: [
        { src: "/agent-identity.js" },
        { src: "https://embed.bunkhouse.ai/embed.js", "data-public-key": "pk_YOUR_SITE_PUBLIC_KEY" },
      ],
    },
    { tagPriority: "critical" },
  );
}

The tested Nuxt integration guide has the complete code for each session mode (signed identity token, server mint and anonymous), CSRF headers and Content Security Policy settings.

When Bunkhouse fits, and when to build it yourself

  • Bunkhouse fits when users want tasks done inside your existing pages, such as filtering, editing or submitting through the controls and API you already have, and you would rather not write and maintain a tool definition for each action, a sandbox for model-written code, an approval UI and usage metering.
  • Build it yourself when the agent must run without a browser page (background jobs, email, chat platforms, scheduled work), when you need to choose the model provider, use your own keys or run inference on your own infrastructure, or when the conversation must be part of your own UI components.
  • Plan for the limits: discovery cannot see server-only routes, workflows the user has not visited, closed shadow roots or cross-origin frames (describe those in a hints file); some controls need a real user gesture; password, payment and file fields are never read, and you can mark other content private.
  • Cost: production use is $49 per month, including $20 of usage credit per paid billing period, then $1 per million input tokens and $3 per million output tokens. See billing.

Try it on your Nuxt app first

A coding agent such as Claude Code, Codex or Cursor can run a free, disposable agent trial on a dummy page of your Nuxt app. No account, card or model API key is needed: your coding agent acts as the assistant's model. A trial lasts 24 hours and allows 100 turns. Give your agent this instruction, changing the address if your app runs elsewhere:

Agent trialPrompt for your coding agent
Read https://api.bunkhouse.ai/agent-trial.md and follow it to run a Bunkhouse agent trial on my dummy Nuxt page at http://localhost:3000.

Use a page with disposable data: everything the assistant reads is sent to your agent.

The trial evaluation checklist shows how to test a read, a rejected write and an approved change, and what a trial does not prove about production use.