<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Erik Hanchett</title>
    <description>The latest articles on DEV Community by Erik Hanchett (@erikch).</description>
    <link>https://dev.to/erikch</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1994%2F0j84YwMs.jpeg</url>
      <title>DEV Community: Erik Hanchett</title>
      <link>https://dev.to/erikch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erikch"/>
    <language>en</language>
    <item>
      <title>10 MCP Servers Worth Adding to Your AI Coding Workflow in 2026</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 01 Sep 2026 22:23:37 +0000</pubDate>
      <link>https://dev.to/erikch/10-mcp-servers-worth-adding-to-your-ai-coding-workflow-in-2026-1j1m</link>
      <guid>https://dev.to/erikch/10-mcp-servers-worth-adding-to-your-ai-coding-workflow-in-2026-1j1m</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I am a big fan of the Model Context Protocol. MCP keeps getting better, and I really think it's the future.&lt;/p&gt;

&lt;p&gt;This list is my favorite MCP servers that I use every day. I picked them based on how useful they are, and how much I've been able to get out of them.&lt;/p&gt;

&lt;p&gt;I also picked them based on these four signals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Public adoption&lt;/li&gt;
&lt;li&gt;Active maintenance&lt;/li&gt;
&lt;li&gt;Official backing&lt;/li&gt;
&lt;li&gt;Whether the server solves a problem developers actually hit&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the video version of the list:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/uiPSA1htycs" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need an MCP-capable coding agent or editor to use these servers. Kiro, Claude Code, Cursor, VS Code, and other clients support different combinations of local and remote MCP connections.&lt;/p&gt;

&lt;p&gt;Keep in mind that whenever you install an MCP server you should be cautious. Don't accept all changes, look at what it's executing first. It's also not a bad idea to try them out on non-production code bases first, just in case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps: the 10 MCP servers worth considering
&lt;/h2&gt;

&lt;p&gt;Here are my top MCP servers, in no particular order.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;MCP server&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Access level to watch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Context7&lt;/td&gt;
&lt;td&gt;Current library documentation&lt;/td&gt;
&lt;td&gt;Low, but verify package resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Chrome DevTools MCP&lt;/td&gt;
&lt;td&gt;Browser debugging and performance&lt;/td&gt;
&lt;td&gt;Full connected-browser access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Playwright MCP&lt;/td&gt;
&lt;td&gt;Repeatable browser automation&lt;/td&gt;
&lt;td&gt;Authenticated pages and local services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;GitHub MCP Server&lt;/td&gt;
&lt;td&gt;Repositories, issues, and pull requests&lt;/td&gt;
&lt;td&gt;Repository and workflow writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Serena&lt;/td&gt;
&lt;td&gt;Semantic code navigation and editing&lt;/td&gt;
&lt;td&gt;Project files and language tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Supabase MCP&lt;/td&gt;
&lt;td&gt;Database and project workflows&lt;/td&gt;
&lt;td&gt;Schema, migration, and data changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;AWS MCP Server&lt;/td&gt;
&lt;td&gt;AWS documentation and APIs&lt;/td&gt;
&lt;td&gt;The caller's IAM permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Vercel MCP&lt;/td&gt;
&lt;td&gt;Deployments, logs, and analytics&lt;/td&gt;
&lt;td&gt;Project and deployment operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Cloudflare API MCP&lt;/td&gt;
&lt;td&gt;Workers, R2, D1, DNS, and APIs&lt;/td&gt;
&lt;td&gt;Broad account API access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Excalidraw MCP&lt;/td&gt;
&lt;td&gt;Editable architecture diagrams&lt;/td&gt;
&lt;td&gt;Diagram content sent to the service&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  1. Context7
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/upstash/context7" rel="noopener noreferrer"&gt;upstash/context7&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Package:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/@upstash/context7-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;@upstash/context7-mcp&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Context7 is one of my favorite MCP servers. I use it all the time to help me look up documentation for my favorite APIs. It's also nice because I don't have to install 100 different skills, I can get it all in one MCP server.&lt;/p&gt;

&lt;p&gt;When I checked, Context7 had 61,446 GitHub stars, 2,960 forks, and 3,975,539 npm downloads during August 2026. Version 4.0.4 shipped on August 28.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It addresses one of the most common AI coding failures without requiring broad access to your project.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Chrome DevTools MCP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/ChromeDevTools/chrome-devtools-mcp" rel="noopener noreferrer"&gt;ChromeDevTools/chrome-devtools-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Chrome DevTools MCP is my browser-debugging pick. It can inspect console messages, network requests, screenshots, Lighthouse results, performance traces, heap data, and the browser state the user actually sees.&lt;/p&gt;

&lt;p&gt;It had 50,274 GitHub stars and 3,538 forks when I checked. The npm package recorded 3,308,596 downloads during August 24 through August 30, 2026. It is an official Google and Chrome DevTools project, and version 1.8.0 shipped on August 25.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It lets the agent debug the running application instead of declaring success when the code merely compiles.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Playwright MCP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/microsoft/playwright-mcp" rel="noopener noreferrer"&gt;microsoft/playwright-mcp&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Package:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/@playwright/mcp" rel="noopener noreferrer"&gt;&lt;code&gt;@playwright/mcp&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Playwright MCP is the browser-automation and testing choice. It had 36,660 GitHub stars, 3,077 forks, and 23,749,682 npm downloads during August 2026. That was the strongest directly attributable package-download signal in my research.&lt;/p&gt;

&lt;p&gt;I go back and forth between the Chrome Devtool and Playwright depending on the situation.&lt;/p&gt;

&lt;p&gt;Chrome DevTools goes deeper on live debugging, network behavior, memory, and performance. Playwright is a cleaner fit when the job is navigating a flow and proving it works repeatedly. Like check out flows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It gives an agent a repeatable way to test user flows instead of relying on a one-time manual check.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. GitHub MCP Server
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/github/github-mcp-server" rel="noopener noreferrer"&gt;github/github-mcp-server&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub's official MCP server is the obvious source-control pick. It can search repositories and code, read issues, work with pull requests, and inspect workflows.&lt;/p&gt;

&lt;p&gt;The repository had 32,626 stars and 4,872 forks when I checked. Version 1.11.0 shipped on August 25, 2026, and GitHub supports hosted, container, and binary connection paths.&lt;/p&gt;

&lt;p&gt;I use this all the time to push up code, or fix merge issues. I go back and forth with this MCP server and using the &lt;code&gt;gh&lt;/code&gt; CLI tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; Source control context appears in almost every software project.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Serena
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/oraios/serena" rel="noopener noreferrer"&gt;oraios/serena&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Package:&lt;/strong&gt; &lt;a href="https://pypi.org/project/serena-agent/" rel="noopener noreferrer"&gt;&lt;code&gt;serena-agent&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Serena is the codebase specialist. Instead of treating a repository as a pile of text files, it uses language tooling for symbol-level retrieval and semantic edits.&lt;/p&gt;

&lt;p&gt;It had 28,686 GitHub stars, 1,934 forks, and 160,865 rolling-month PyPI downloads reported on August 31, 2026. Version 1.7.0 shipped on August 9.&lt;/p&gt;

&lt;p&gt;I have not used Serena a lot, but from what I can tell it's really useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It gives agents a structured view of code when generic file search is not enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Supabase MCP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/supabase/mcp" rel="noopener noreferrer"&gt;supabase/mcp&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Package:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/@supabase/mcp-server-supabase" rel="noopener noreferrer"&gt;&lt;code&gt;@supabase/mcp-server-supabase&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Supabase MCP connects the agent to project, database, schema, and migration workflows. The repository had 2,883 stars and 403 forks, while the npm package recorded 439,749 downloads during August 2026.&lt;/p&gt;

&lt;p&gt;This is where the list becomes very tool specific. If you are using Supabase, use this tool!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It gives a Supabase project direct backend context, but only projects already using Supabase need it.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. AWS MCP Server
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://docs.aws.amazon.com/agent-toolkit/latest/userguide/mcp-server.html" rel="noopener noreferrer"&gt;AWS MCP Server&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Toolkit:&lt;/strong&gt; &lt;a href="https://github.com/aws/agent-toolkit-for-aws" rel="noopener noreferrer"&gt;aws/agent-toolkit-for-aws&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AWS is the cloud option I wanted to spend time on it because it's my favorite! The &lt;a href="https://github.com/aws/agent-toolkit-for-aws" rel="noopener noreferrer"&gt;Agent Toolkit for AWS&lt;/a&gt; is larger than one MCP server. It combines MCP configuration with skills, plugins, and agent integrations.&lt;/p&gt;

&lt;p&gt;The managed AWS MCP Server can search current AWS documentation and call AWS APIs across more than 300 services. The skill tells the agent how to approach a task. The server supplies current information and runtime tools. Same ecosystem, different jobs.&lt;/p&gt;

&lt;p&gt;Check it out!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It combines current AWS knowledge with live cloud inspection while retaining familiar IAM and audit controls.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Vercel MCP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://vercel.com/docs/mcp/vercel-mcp" rel="noopener noreferrer"&gt;Vercel MCP&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vercel MCP is a hosted server for teams already deploying on Vercel. It can inspect projects, deployments, build and runtime logs, analytics, and traces. It can also create deployments and perform collaboration operations.&lt;/p&gt;

&lt;p&gt;Be aware that &lt;a href="https://github.com/vercel/mcp-handler" rel="noopener noreferrer"&gt;&lt;code&gt;vercel/mcp-handler&lt;/code&gt;&lt;/a&gt; is a framework for building MCP servers. The AI SDK includes an MCP client. Vercel also ships an agent plugin. None of those is the hosted Vercel MCP server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It brings deployment state and production diagnosis into the agent for projects that already run on Vercel.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Cloudflare API MCP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/cloudflare/mcp" rel="noopener noreferrer"&gt;cloudflare/mcp&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Catalog:&lt;/strong&gt; &lt;a href="https://developers.cloudflare.com/agents/model-context-protocol/mcp-servers-for-cloudflare/" rel="noopener noreferrer"&gt;Cloudflare MCP servers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cloudflare's API MCP covers Workers, R2, D1, DNS, deployments, and roughly 2,500 Cloudflare API endpoints.&lt;/p&gt;

&lt;p&gt;Vercel and Cloudflare are alternatives in this list. Pick the server for the platform that runs your project rather than installing both because they appear in a roundup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It exposes a broad platform API without filling the context window with thousands of tools at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Excalidraw MCP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/excalidraw/excalidraw-mcp" rel="noopener noreferrer"&gt;excalidraw/excalidraw-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Excalidraw is an amazing drawing tool, and it's worth adding to this list. Its first-party server had 5,212 stars and 487 forks when I checked. Version 0.3.2 shipped in February 2026, with code activity cited through March.&lt;/p&gt;

&lt;p&gt;I use it all the time to help me create architecture diagrams!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is good:&lt;/strong&gt; It turns project context into an editable artifact that can leave the editor and help a team communicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about Canva and Figma?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://canva.dev/docs/mcp/" rel="noopener noreferrer"&gt;Canva MCP&lt;/a&gt; is useful when you need thumbnails, launch graphics, presentations, or other design assets. Canva also has a separate Dev MCP for people building Canva apps and integrations. It does not publish a comparable server-specific adoption number, and it sits farther from the code, so I treat it as a bonus rather than one of the ten core coding picks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developers.figma.com/docs/figma-mcp-server/" rel="noopener noreferrer"&gt;Figma MCP&lt;/a&gt; is the design-to-code alternative. It can supply design context, Code Connect mappings, assets, and diagram information. It makes sense when the implementation starts from a Figma design.&lt;/p&gt;

&lt;p&gt;If you need design, check both of them out!&lt;/p&gt;

&lt;h2&gt;
  
  
  Three popular names I left off
&lt;/h2&gt;

&lt;p&gt;Here are a few other MCP servers I left out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Puppeteer MCP
&lt;/h3&gt;

&lt;p&gt;The historical &lt;a href="https://www.npmjs.com/package/@modelcontextprotocol/server-puppeteer" rel="noopener noreferrer"&gt;&lt;code&gt;@modelcontextprotocol/server-puppeteer&lt;/code&gt;&lt;/a&gt; package is deprecated, and its source lives in the &lt;a href="https://github.com/modelcontextprotocol/servers-archived/tree/main/src/puppeteer" rel="noopener noreferrer"&gt;archived reference-server repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It still recorded 28,716 npm downloads during August 24 through August 30, 2026. That is evidence that package downloads can outlive maintenance, not a reason to recommend it for a new setup. Use Chrome DevTools MCP or Playwright MCP instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory reference server
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/modelcontextprotocol/servers/tree/main/src/memory" rel="noopener noreferrer"&gt;Knowledge Graph Memory server&lt;/a&gt; is useful for learning how basic MCP persistence works. Its maintainers describe the reference servers as educational examples rather than production-ready products.&lt;/p&gt;

&lt;p&gt;The package recorded 74,597 weekly downloads, but the roughly 89,000 stars belong to the entire &lt;code&gt;modelcontextprotocol/servers&lt;/code&gt; repository. They cannot be assigned to Memory alone. Every connected client can also read or change its local JSONL graph, which may accumulate sensitive project data.&lt;/p&gt;

&lt;h3&gt;
  
  
  TanStack Intent
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/TanStack/intent" rel="noopener noreferrer"&gt;TanStack Intent&lt;/a&gt; discovers and packages versioned Agent Skills. I like the project, and it is relevant to AI coding, but it is not an MCP server, client, or registry.&lt;/p&gt;

&lt;p&gt;The distinction matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Skills provide instructions and procedural knowledge loaded into the agent.&lt;/li&gt;
&lt;li&gt;MCP provides runtime tools, resources, and live external data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS and Vercel ship products across both categories, which is why the names can blur together. Useful does not automatically mean MCP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finale
&lt;/h2&gt;

&lt;p&gt;I use many of these MCP servers every day. Try a few out and see how you like them.&lt;/p&gt;

&lt;p&gt;Today my starter stack looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context7&lt;/strong&gt; for current APIs and examples&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome DevTools MCP or Playwright MCP&lt;/strong&gt; for verification&lt;/li&gt;
&lt;li&gt;**GitHub, and AWS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add Excalidraw, Canva, or Figma when the work needs to become a diagram or design.&lt;/p&gt;

&lt;p&gt;The full set of research links and measurement windows is available in the &lt;a href="https://www.youtube.com/watch?v=uiPSA1htycs" rel="noopener noreferrer"&gt;video description&lt;/a&gt;. Which MCP server has stayed in your setup after the first week?&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Build a Full-Stack Music Station with OpenRouter, Amazon Bedrock, and Nuxt</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Wed, 26 Aug 2026 00:48:03 +0000</pubDate>
      <link>https://dev.to/aws/build-a-full-stack-music-station-with-openrouter-amazon-bedrock-and-nuxt-5fh4</link>
      <guid>https://dev.to/aws/build-a-full-stack-music-station-with-openrouter-amazon-bedrock-and-nuxt-5fh4</guid>
      <description>&lt;p&gt;Have you ever been coding and then gotten into that flow state? You know where hours pass by , and it feels to you it's only been a few minutes? Me too. One thing that really helps me get into that state is music. So I create my own music Lo-Fi server called compile and chill. &lt;/p&gt;

&lt;p&gt;As a part of this project, I created three radio stations. Each station can generate a 16:9 scene with &lt;a href="https://aws.amazon.com/bedrock/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt;, compose an instrumental loop with ElevenLabs, and turn an illustration into a six-second video through OpenRouter. Generated files live in private &lt;a href="https://aws.amazon.com/s3/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon S3&lt;/a&gt; storage and return to the browser through the Nuxt server.&lt;/p&gt;

&lt;p&gt;I also added a Stream Deck API interface! &lt;/p&gt;

&lt;p&gt;This tutorial shows how to build this radio station from start to finish. &lt;/p&gt;

&lt;p&gt;The complete source code is available in the &lt;a href="https://github.com/ErikCH/compile-and-chill" rel="noopener noreferrer"&gt;Compile &amp;amp; Chill repository&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/6tEVRqptHmg" width="100%" height="390"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=6tEVRqptHmg" rel="noopener noreferrer"&gt;Watch the full video on YouTube&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need the following tools for the complete build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 22.19 or newer. The locked Nuxt 4.5.2 release requires Node 22.19+, 24.11+, or 26+.&lt;/li&gt;
&lt;li&gt;npm 10 or newer.&lt;/li&gt;
&lt;li&gt;An AWS account and a configured &lt;a href="https://aws.amazon.com/cli/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS Command Line Interface (AWS CLI)&lt;/a&gt; profile.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS Serverless Application Model (AWS SAM) CLI&lt;/a&gt; for the private storage stack.&lt;/li&gt;
&lt;li&gt;Access to Stability AI Stable Image Ultra through Amazon Bedrock in &lt;code&gt;us-west-2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An ElevenLabs API key for music generation.&lt;/li&gt;
&lt;li&gt;An OpenRouter API key for animated scenes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The provider credentials are optional. Without them, the UI, bundled scene, station switching, player, and Focus Block timer still work.&lt;/p&gt;

&lt;p&gt;The identity running the app needs &lt;code&gt;bedrock:InvokeModel&lt;/code&gt; plus bucket-scoped permissions for &lt;code&gt;s3:GetObject&lt;/code&gt;, &lt;code&gt;s3:PutObject&lt;/code&gt;, &lt;code&gt;s3:DeleteObject&lt;/code&gt;, &lt;code&gt;s3:DeleteObjectVersion&lt;/code&gt;, and &lt;code&gt;s3:ListBucketVersions&lt;/code&gt;. Use a role or profile scoped to the station bucket rather than an administrator identity.&lt;/p&gt;

&lt;p&gt;For this project I included infrastructure as code with SAM to help setup the AWS parts. It's also included in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Run the station without credentials
&lt;/h3&gt;

&lt;p&gt;Pull down the repo and get started!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ErikCH/compile-and-chill.git
&lt;span class="nb"&gt;cd &lt;/span&gt;compile-and-chill
npm ci
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://127.0.0.1:8231&lt;/code&gt;. You should see the station UI with the bundled placeholder scene.&lt;/p&gt;

&lt;p&gt;The environment file separates each feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ELEVENLABS_API_KEY=
BEDROCK_IMAGE_REGION=us-west-2
BEDROCK_IMAGE_MODEL_ID=stability.stable-image-ultra-v1:1
AWS_PROFILE=default
STATION_S3_BUCKET=
STATION_S3_REGION=us-west-2
OPENROUTER_API_KEY=
STATION_BIND_HOST=127.0.0.1
NUXT_CONTROL_TOKEN=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not add &lt;code&gt;NUXT_PUBLIC_&lt;/code&gt; to these names. Nuxt exposes public runtime configuration to browser code.&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;STATION_BIND_HOST&lt;/code&gt; on loopback unless you need remote control. For Stream Deck access from another machine, bind to a private VPN interface address, never &lt;code&gt;0.0.0.0&lt;/code&gt; or a public IP, and set a long random &lt;code&gt;NUXT_CONTROL_TOKEN&lt;/code&gt;. The server refuses a non-loopback binding without that token.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Put provider calls behind Nuxt server routes
&lt;/h3&gt;

&lt;p&gt;The browser should call your application. Compile &amp;amp; Chill keeps provider credentials in Nuxt &lt;code&gt;runtimeConfig&lt;/code&gt; and places provider code under &lt;code&gt;server/api/&lt;/code&gt; and &lt;code&gt;server/utils/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fouhe9zlk3lg60u72i7m6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fouhe9zlk3lg60u72i7m6.png" alt="Architecture diagram showing the browser calling Nuxt server routes, which connect to Amazon Bedrock, ElevenLabs, OpenRouter, and private Amazon S3 storage" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The relevant section of &lt;code&gt;nuxt.config.ts&lt;/code&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;runtimeConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;elevenLabsApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ELEVENLABS_API_KEY&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;imageGenerationRegion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BEDROCK_IMAGE_REGION&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-west-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;imageGenerationModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BEDROCK_IMAGE_MODEL_ID&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stability.stable-image-ultra-v1:1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stationStorageBucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STATION_S3_BUCKET&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stationStorageRegion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STATION_S3_REGION&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-west-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;openRouterApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENROUTER_API_KEY&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This boundary gives you one place to validate requests, clamp paid parameters, redact signed URLs, and translate provider errors into useful HTTP responses.&lt;/p&gt;

&lt;p&gt;You can inspect the complete routes in &lt;a href="https://github.com/ErikCH/compile-and-chill/tree/21c60e6508f6f9c1fbda21ceab18b60a14b20aa1/server/api" rel="noopener noreferrer"&gt;&lt;code&gt;server/api&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Generate a scene with Amazon Bedrock
&lt;/h3&gt;

&lt;p&gt;Authenticate with your AWS profile first. This example uses &lt;a href="https://aws.amazon.com/iam/identity-center/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS IAM Identity Center&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sso login &lt;span class="nt"&gt;--profile&lt;/span&gt; your-profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the profile and model settings to &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS_PROFILE=your-profile
BEDROCK_IMAGE_REGION=us-west-2
BEDROCK_IMAGE_MODEL_ID=stability.stable-image-ultra-v1:1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image route validates the station mode, visual style, developer presentation, and optional seed. It then calls &lt;code&gt;generateStationScene()&lt;/code&gt; in &lt;a href="https://github.com/ErikCH/compile-and-chill/blob/21c60e6508f6f9c1fbda21ceab18b60a14b20aa1/server/utils/station-generation.ts" rel="noopener noreferrer"&gt;&lt;code&gt;server/utils/station-generation.ts&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The provider payload is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildScenePrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;visualStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;developer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;diversitySeed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="c1"&gt;// Shortened here. The source contains the complete negative prompt.&lt;/span&gt;
  &lt;span class="na"&gt;negative_prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logos, readable text, watermark, rear view, motion blur, extra fingers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;visualStyle&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;illustrated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;photograph, live action, photorealistic skin, 3D render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cartoon, anime, cel shading, flat illustration&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-to-image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;aspect_ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;16:9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;output_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InvokeModelCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageGenerationModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server normalizes the seed before constructing the payload. This matters because &lt;code&gt;Number(undefined)&lt;/code&gt; becomes &lt;code&gt;NaN&lt;/code&gt;, and JSON serializes &lt;code&gt;NaN&lt;/code&gt; as &lt;code&gt;null&lt;/code&gt;. Stable Image Ultra expects an integer.&lt;/p&gt;

&lt;p&gt;The prompt also ties visual style to output type. Static scenes use a realistic style. Animated scenes use a clearly illustrated 2D style so the source is visibly non-photorealistic. I added that distinction after a provider's person-likeness classifier refused one of the illustrated anchors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint:&lt;/strong&gt; restart the app, open Visual settings, choose Realistic static, and generate one scene. If the model is unavailable in the configured region, the route returns the provider error and model ID instead of silently switching models.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Add looping music with ElevenLabs
&lt;/h3&gt;

&lt;p&gt;The UI sends a 60-second duration, and the server should reject or normalize nonnumeric input before applying its 10-to-60-second bounds. This hardened version avoids sending &lt;code&gt;null&lt;/code&gt; to the provider if another client calls the route with an invalid value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestedDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;durationSeconds&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;durationSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestedDuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requestedDuration&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.elevenlabs.io/v1/music?output_format=mp3_44100_128&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xi-api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elevenLabsApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildMusicPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;music_length_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;durationSeconds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;music_v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;generation_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;force_instrumental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sign_with_c2pa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;generation_mode: 'loop'&lt;/code&gt; is one part of making the repeat sound natural. The prompt also asks for no intro pickup, ending cadence, or fade-out. A track written like a normal song sounds broken when it jumps from the ending back to the first beat.&lt;/p&gt;

&lt;p&gt;Compile &amp;amp; Chill keeps one generated track per station mode. Switching from Deep Work to Rainy Debug and back reuses the existing track instead of making another paid request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint:&lt;/strong&gt; add &lt;code&gt;ELEVENLABS_API_KEY&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt;, restart the server, and generate one 60-second track. Switch modes and confirm that returning to the original mode reuses its audio.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Deploy the private media library
&lt;/h3&gt;

&lt;p&gt;The repository includes an AWS SAM template managed by &lt;a href="https://aws.amazon.com/cloudformation/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS CloudFormation&lt;/a&gt;. Validate and deploy it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam validate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-file&lt;/span&gt; infra/compile-and-chill-private-station.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2

sam deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-file&lt;/span&gt; infra/compile-and-chill-private-station.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-confirm-changeset&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-fail-on-empty-changeset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the media bucket output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Stacks[0].Outputs[?OutputKey=='StationMediaBucketName'].OutputValue"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy that value into &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STATION_S3_BUCKET=YOUR_STACK_OUTPUT
STATION_S3_REGION=us-west-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stack creates two versioned buckets. One stores generated station media. The other stores &lt;a href="https://aws.amazon.com/cloudtrail/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS CloudTrail&lt;/a&gt; data-event logs encrypted with &lt;a href="https://aws.amazon.com/kms/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS Key Management Service (AWS KMS)&lt;/a&gt;. An &lt;a href="https://aws.amazon.com/cloudwatch/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon CloudWatch&lt;/a&gt; alarm watches delete requests against the media bucket.&lt;/p&gt;

&lt;p&gt;The application assigns each browser profile a random station ID in an HttpOnly, SameSite cookie. Objects use this layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stations/&amp;lt;station-id&amp;gt;/manifest.json
stations/&amp;lt;station-id&amp;gt;/&amp;lt;mode&amp;gt;/scene.png
stations/&amp;lt;station-id&amp;gt;/&amp;lt;mode&amp;gt;/scene.mp4
stations/&amp;lt;station-id&amp;gt;/&amp;lt;mode&amp;gt;/music.mp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser receives same-origin URLs such as &lt;code&gt;/api/library/assets/deepWork/scene&lt;/code&gt;. The route reads the matching S3 object into server memory and returns it. The current implementation is a proxy, not a streaming pass-through, so account for memory when increasing the 50 MiB video limit.&lt;/p&gt;

&lt;p&gt;The storage implementation is in &lt;a href="https://github.com/ErikCH/compile-and-chill/blob/21c60e6508f6f9c1fbda21ceab18b60a14b20aa1/server/utils/station-library.ts" rel="noopener noreferrer"&gt;&lt;code&gt;server/utils/station-library.ts&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint:&lt;/strong&gt; regenerate a scene or track, reload the page in the same browser profile, and confirm that the media returns. Open a private browsing window and confirm that it starts with a separate station.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Turn one illustration into a seamless video loop
&lt;/h3&gt;

&lt;p&gt;Animated mode generates a fresh illustrated anchor with Amazon Bedrock, saves it to S3, and creates a 300-second presigned read URL. OpenRouter needs that temporary URL because its video provider must download the input image.&lt;/p&gt;

&lt;p&gt;The trick is to submit the same anchor as both frame constraints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;720p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;aspect_ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;16:9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;generate_audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Animate this 2D cartoon illustration of a fictional character.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Use a locked-off camera and keep the illustrated composition unchanged.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The provided illustration is both the first and last frame.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Allow only tiny two-hand typing motion and one gentle blink.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No zoom, pan, cut, identity change, new objects, or geometry changes.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Create a silent seamless six-second ambient loop.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;frame_images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sourceUrl&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;frame_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first_frame&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sourceUrl&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;frame_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;last_frame&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ryc46qkmezlhtirqt03.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ryc46qkmezlhtirqt03.png" alt="Seven-step animation pipeline showing an Amazon Bedrock anchor saved to Amazon S3, signed for five minutes, used as both video endpoints, polled, downloaded, and saved" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The model can move away from the anchor, but the matching frame constraints request a return to the same composition at the end. The prompt limits movement to a small typing motion and one blink, with no camera movement, reframing, cuts, new objects, or identity changes.&lt;/p&gt;

&lt;p&gt;The app discovers compatible video models at runtime. A candidate must support six seconds, 720p, &lt;code&gt;first_frame&lt;/code&gt;, and &lt;code&gt;last_frame&lt;/code&gt;. It prefers Seedance 2.0 Fast, then checks a short fallback list.&lt;/p&gt;

&lt;p&gt;The fallback policy is intentionally narrow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP &lt;code&gt;402&lt;/code&gt; stops because credits are unavailable.&lt;/li&gt;
&lt;li&gt;HTTP &lt;code&gt;429&lt;/code&gt; stops because the account is rate limited.&lt;/li&gt;
&lt;li&gt;An input-image moderation refusal may try the next compatible model because no video job was created.&lt;/li&gt;
&lt;li&gt;Other failures stop rather than risk submitting and billing a duplicate job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See the complete policy in &lt;a href="https://github.com/ErikCH/compile-and-chill/blob/21c60e6508f6f9c1fbda21ceab18b60a14b20aa1/server/utils/openrouter-video.ts" rel="noopener noreferrer"&gt;&lt;code&gt;server/utils/openrouter-video.ts&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Video generation takes minutes rather than seconds, and provider timing and prices change. Treat the request as a paid background job even if the first version runs inside one long HTTP request.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Report real progress during a long request
&lt;/h3&gt;

&lt;p&gt;A spinner cannot tell the user whether a three-minute provider job is moving or stuck. Compile &amp;amp; Chill keeps a process-local job registry with six phases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;creating anchor
preparing source
submitting video
rendering loop
downloading loop
saving privately
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser creates a job ID, starts the generation request, and polls the Nuxt status endpoint every 750 milliseconds. Nuxt polls the provider every 15 seconds, up to 48 times. These are separate loops.&lt;/p&gt;

&lt;p&gt;The registry also rejects a second active job for the same station with HTTP &lt;code&gt;409&lt;/code&gt;. That protects the user from duplicate clicks, but it is not a distributed queue. A process restart loses status, and multiple Nuxt instances would each have their own registry. For a multi-instance deployment, move job state and concurrency control to a shared data store and worker queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Validate the build
&lt;/h3&gt;

&lt;p&gt;Run the same checks used for the source project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm run typecheck
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current repository has 26 passing tests plus a clean type check and production build.&lt;/p&gt;

&lt;p&gt;Then test one provider at a time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with no credentials and verify the UI.&lt;/li&gt;
&lt;li&gt;Add Amazon Bedrock and generate one static scene.&lt;/li&gt;
&lt;li&gt;Add ElevenLabs and generate one track.&lt;/li&gt;
&lt;li&gt;Deploy the storage stack and verify reload persistence.&lt;/li&gt;
&lt;li&gt;Add OpenRouter last, acknowledge the paid operation, and generate one animated loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This order keeps failures small. If the video path fails, you already know that image generation, storage, and browser identity work independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;The cleanup steps remove data permanently. Download anything you want to keep before continuing. The buckets and KMS key use retention policies, so they can remain after stack deletion and may continue to incur charges.&lt;/p&gt;

&lt;p&gt;First, use &lt;strong&gt;Delete saved station&lt;/strong&gt; inside each browser profile whose generated media should be removed. This purges object versions and delete markers under that profile's station prefix.&lt;/p&gt;

&lt;p&gt;Before deleting the stack, record the retained resource names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MEDIA_BUCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Stacks[0].Outputs[?OutputKey=='StationMediaBucketName'].OutputValue"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;AUDIT_BUCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Stacks[0].Outputs[?OutputKey=='CloudTrailLogBucketName'].OutputValue"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;KMS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws kms describe-key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-id&lt;/span&gt; &lt;span class="nb"&gt;alias&lt;/span&gt;/compile-and-chill-cloudtrail &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; KeyMetadata.KeyId &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete the non-retained stack resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam delete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The media bucket, audit bucket, and KMS key are retained by design. Empty &lt;strong&gt;all versions and delete markers&lt;/strong&gt; from both versioned buckets in the S3 console, then delete the buckets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api delete-bucket &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MEDIA_BUCKET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2
aws s3api delete-bucket &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AUDIT_BUCKET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schedule the retained KMS key for deletion after confirming that you no longer need the encrypted audit logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws kms schedule-key-deletion &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KMS_KEY_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pending-window-in-days&lt;/span&gt; 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;KMS key deletion has a waiting period. After deletion, data encrypted only by that key cannot be recovered.&lt;/p&gt;

&lt;p&gt;For local cleanup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; node_modules .nuxt .output
&lt;span class="nb"&gt;rm&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Finale
&lt;/h2&gt;

&lt;p&gt;This has been a very fun project. Let me know if you try it out!&lt;/p&gt;

&lt;p&gt;You can explore every route, prompt, infrastructure resource, and test in the &lt;a href="https://github.com/ErikCH/compile-and-chill" rel="noopener noreferrer"&gt;Compile &amp;amp; Chill source code&lt;/a&gt;. For another Nuxt project that keeps AI actions behind explicit user approval, read &lt;a href="https://dev.to/aws/how-to-build-an-ai-agent-that-asks-permission-first-nuxt-ai-sdk-7-n42"&gt;How to Build an AI Agent That Asks Permission First&lt;/a&gt;. Leave a comment on what you think! Thank!&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>aws</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Build an AI Agent That Asks Permission First (Nuxt + AI SDK 7)</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 18 Aug 2026 18:38:25 +0000</pubDate>
      <link>https://dev.to/aws/how-to-build-an-ai-agent-that-asks-permission-first-nuxt-ai-sdk-7-n42</link>
      <guid>https://dev.to/aws/how-to-build-an-ai-agent-that-asks-permission-first-nuxt-ai-sdk-7-n42</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I did something stupid. I built a superhero-themed Nuxt app, connected it to an Anthropic model through &lt;a href="https://aws.amazon.com/bedrock/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt;, and gave it a tool that deletes files from my computer. In fact, if I wasn't careful, it could have deleted all my files!&lt;/p&gt;

&lt;p&gt;The first time I tried it, I didn't use any sort of approval mechanism. And as you expected it just deleted things. Then I looked into how my coding agent works, and I learned about tool approvals.&lt;/p&gt;

&lt;p&gt;I learned that &lt;a href="https://vercel.com/blog/ai-sdk-7" rel="noopener noreferrer"&gt;AI SDK 7&lt;/a&gt; has a tool approval at the model-call level. It works by pausing for an approval, showing an approval window, and then deleting it. I then put &lt;a href="https://kiro.dev/docs/cli/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro CLI&lt;/a&gt; behind the same interface using Agent Client Protocol (ACP).&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/RktwtjobGI4"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=RktwtjobGI4" rel="noopener noreferrer"&gt;Watch the full video on YouTube&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 22 or later. AI SDK 7 requires Node.js 22 and uses ECMAScript modules (ESM).&lt;/li&gt;
&lt;li&gt;npm 11 or another package manager that works with Nuxt 4.&lt;/li&gt;
&lt;li&gt;AWS credentials available through the standard provider chain.&lt;/li&gt;
&lt;li&gt;Access to an Amazon Bedrock model in your AWS Region.&lt;/li&gt;
&lt;li&gt;The AWS CLI if you want to list the inference profiles available to your account.&lt;/li&gt;
&lt;li&gt;An authenticated Kiro CLI installation for the optional ACP section.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Create the Nuxt app
&lt;/h2&gt;

&lt;p&gt;Create the project and install the versions used in the recorded demo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nuxi@latest init nuxt-agent-approval
&lt;span class="nb"&gt;cd &lt;/span&gt;nuxt-agent-approval

npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  nuxt@4.5.2 &lt;span class="se"&gt;\&lt;/span&gt;
  vue@3.5.41 &lt;span class="se"&gt;\&lt;/span&gt;
  ai@7.0.66 &lt;span class="se"&gt;\&lt;/span&gt;
  @ai-sdk/vue@4.0.66 &lt;span class="se"&gt;\&lt;/span&gt;
  @ai-sdk/amazon-bedrock@5.0.57 &lt;span class="se"&gt;\&lt;/span&gt;
  @aws-sdk/credential-providers@3.1111.0 &lt;span class="se"&gt;\&lt;/span&gt;
  @nuxt/ui@4.10.0 &lt;span class="se"&gt;\&lt;/span&gt;
  zod@4.4.3

npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @iconify-json/lucide@1.2.123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register Nuxt UI and expose the Amazon Bedrock settings through server-side runtime config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// nuxt.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nuxt/ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;css&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~/assets/css/main.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;runtimeConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;awsRegion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AWS_REGION&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-west-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bedrockModelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NUXT_BEDROCK_MODEL_ID&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the two Nuxt UI imports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* app/assets/css/main.css */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"@nuxt/ui"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can compare your setup with the &lt;a href="https://github.com/ErikCH/blog-posts/tree/main/apps/ai-sdk-7-nuxt-agents" rel="noopener noreferrer"&gt;complete companion project&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Keep the file tool inside a fixture directory
&lt;/h2&gt;

&lt;p&gt;The video uses two fixture files, &lt;code&gt;old-draft.md&lt;/code&gt; and &lt;code&gt;keep-me.md&lt;/code&gt;. Create them before adding the tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; fixtures
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'# Old draft\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fixtures/old-draft.md
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'# Keep me\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fixtures/keep-me.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent can list or remove files in that directory, but it should not accept a path such as &lt;code&gt;../../package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Approval decides whether a tool runs. It does not decide what the tool can reach after it starts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/utils/file-tools.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lstat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readdir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;realpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isAbsolute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveInsideFixtures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixtures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inputPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;isAbsolute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;createError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;statusMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Path escapes the fixtures directory: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;inputPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;List the files in the project fixtures directory.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({}),&lt;/span&gt;
  &lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixtures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;withFileTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFile&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deleteFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Permanently delete one file from the fixtures directory.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolveInsideFixtures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;lstat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;isFile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSymbolicLink&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;deleted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Not a regular file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;realpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixtures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canonicalTarget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;realpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canonicalTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;isAbsolute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;createError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;statusMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;File resolves outside fixtures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canonicalTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;deleted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second check happens after &lt;code&gt;realpath()&lt;/code&gt;. That catches a path that looked local before resolution but points outside the fixture directory through a symbolic link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Connect the route to Amazon Bedrock
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I used Amazon Bedrock, but you can use provider with AI-SDK&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create the provider in &lt;code&gt;server/utils/bedrock.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createAmazonBedrock&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ai-sdk/amazon-bedrock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fromNodeProviderChain&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-sdk/credential-providers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useBedrock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;awsRegion&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRuntimeConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createAmazonBedrock&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;awsRegion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;credentialProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fromNodeProviderChain&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fromNodeProviderChain()&lt;/code&gt; uses the AWS credentials already available to your local environment, including &lt;a href="https://aws.amazon.com/iam/identity-center/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS IAM Identity Center&lt;/a&gt; sessions, named profiles, environment variables, and instance roles. You do not need to put a long-lived access key in the Nuxt project.&lt;/p&gt;

&lt;p&gt;Do not copy a model ID from this post. Available IDs vary by account and AWS Region. List the active &lt;a href="https://aws.amazon.com/blogs/machine-learning/getting-started-with-cross-region-inference-in-amazon-bedrock/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock inference profiles&lt;/a&gt; for your account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws bedrock list-inference-profiles &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'inferenceProfileSummaries[?status==`ACTIVE`].inferenceProfileId'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set one of the returned profile IDs before starting Nuxt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-west-2
&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Inference profile ID: "&lt;/span&gt; NUXT_BEDROCK_MODEL_ID
&lt;span class="nb"&gt;export &lt;/span&gt;NUXT_BEDROCK_MODEL_ID
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now add an unguarded chat route. Starting without approval makes the failure visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/api/chat.post.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;stepCountIs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;convertToModelMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;toUIMessageStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createUIMessageStreamResponse&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AmazonBedrockProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ai-sdk/amazon-bedrock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BedrockModelId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AmazonBedrockProvider&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bedrockModelId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRuntimeConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bedrock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useBedrock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bedrockModelId&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;BedrockModelId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Manage files in the fixture project. List files before deleting. Never guess a filename.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;convertToModelMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;listFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteFile&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;stopWhen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stepCountIs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toUIMessageStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createUIMessageStreamResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stopWhen&lt;/code&gt; is important. AI SDK 7 stops after one step by default. The model can call &lt;code&gt;deleteFile&lt;/code&gt;, receive the result, and then stop before it tells the user what happened. Five steps leave room to list, delete, and summarize while keeping the loop bounded.&lt;/p&gt;

&lt;p&gt;At this point, &lt;code&gt;delete old-draft.md&lt;/code&gt; removes the file as soon as the model selects the tool. That is what happened in the first minute of the video.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Add approval to the delete tool
&lt;/h2&gt;

&lt;p&gt;Add one option to the &lt;code&gt;streamText()&lt;/code&gt; call (toolApproval):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bedrockModelId&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;BedrockModelId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Manage files in the fixture project. List files before deleting. Never guess a filename.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;convertToModelMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;listFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteFile&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;stopWhen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stepCountIs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;toolApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;deleteFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-approval&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The option lives on &lt;code&gt;streamText()&lt;/code&gt;, not inside the tool definition. The same &lt;code&gt;deleteFile&lt;/code&gt; tool might run unattended in a maintenance job and require a person in a customer-facing chat. &lt;/p&gt;

&lt;p&gt;AI SDK 7 supports more than a yes-or-no policy. A policy function can approve a call, deny it without asking, or send it to the user. This example uses the direct &lt;code&gt;user-approval&lt;/code&gt; status because every delete should stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Render the approval request in Nuxt
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ui.nuxt.com/docs/components/chat" rel="noopener noreferrer"&gt;Nuxt UI's chat documentation&lt;/a&gt; follows the same AI SDK message-part model. The recorded app uses &lt;code&gt;useChat()&lt;/code&gt; from &lt;code&gt;@ai-sdk/vue&lt;/code&gt; and the &lt;code&gt;isToolApprovalPending()&lt;/code&gt; helper from Nuxt UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;DefaultChatTransport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;getToolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isTextUIPart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isToolUIPart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;lastAssistantMessageIsCompleteWithApprovalResponses&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useChat&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ai-sdk/vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isToolApprovalPending&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nuxt/ui/utils/ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;addToolApprovalResponse&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useChat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DefaultChatTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;sendAutomaticallyWhen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lastAssistantMessageIsCompleteWithApprovalResponses&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sendAutomaticallyWhen&lt;/code&gt; resumes the interrupted turn after the user answers. Without it, the approval state changes in the browser but the agent does not continue on its own.&lt;/p&gt;

&lt;p&gt;Render tool parts and attach the two decisions while approval is pending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"message in messages"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"message.id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt;
      &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"(part, index) in message.parts"&lt;/span&gt;
      &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"`$&lt;/span&gt;{message.id}-${part.type}-${index}`"
    &amp;gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"isToolUIPart(part)"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"tool-card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nf"&gt;getToolName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;pre&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"part.input"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"isToolApprovalPending(part)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
            &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
            &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"addToolApprovalResponse(&lt;/span&gt;{
              id: part.approval!.id,
              approved: true
            })"
          &amp;gt;
            Do it
          &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

          &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
            &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
            &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"addToolApprovalResponse(&lt;/span&gt;{
              id: part.approval!.id,
              approved: false,
              reason: 'The user declined this file deletion.'
            })"
          &amp;gt;
            Nope
          &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"isTextUIPart(part)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;submit.prevent=&lt;/span&gt;&lt;span class="s"&gt;"onSubmit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"input"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"delete old-draft.md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;:disabled=&lt;/span&gt;&lt;span class="s"&gt;"status !== 'ready'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FYI, An approval button is not useful when the person cannot see which path the agent wants to remove.&lt;/p&gt;

&lt;p&gt;The denial reason is also useful. &lt;code&gt;approved: false&lt;/code&gt; tells the model it cannot run the tool. The reason gives it enough context to explain what happened rather than trying the same call again.&lt;/p&gt;

&lt;p&gt;Run the prompt twice. Deny it once and confirm that &lt;code&gt;old-draft.md&lt;/code&gt; remains. Reset the fixtures, repeat the prompt, approve it, and confirm that the file disappears. The model received the same request both times. &lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Put Kiro CLI behind the same Nuxt UI
&lt;/h2&gt;

&lt;p&gt;The video switches the backend for the final demo. Instead of sending the prompt directly to an Anthropic model through Amazon Bedrock, the Nuxt app talks to Kiro CLI over &lt;a href="https://kiro.dev/docs/cli/acp/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Agent Client Protocol&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;ACP gives the app a common way to start an agent session, send a prompt, receive tool events, and answer permission requests. Kiro runs as a separate process and keeps its existing agent tools and Model Context Protocol (MCP) integrations. The Nuxt app remains responsible for the interface and the host tools it exposes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F343y1kh8jr47vrby673y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F343y1kh8jr47vrby673y.png" alt="ACP Image" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install the AI SDK harness packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  @ai-sdk/harness@1.0.73 &lt;span class="se"&gt;\&lt;/span&gt;
  @ai-sdk/harness-acp@1.0.11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Kiro route creates a &lt;code&gt;HarnessAgent&lt;/code&gt; instead of calling &lt;code&gt;streamText()&lt;/code&gt; directly. The &lt;code&gt;createKiroHarness()&lt;/code&gt; ACP preset and &lt;code&gt;createUnsafeLocalSandbox()&lt;/code&gt; development adapter come from the companion project, so treat this as the route configuration rather than a standalone file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HarnessAgent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ai-sdk/harness/agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kiroExecutable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;which&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kiro-cli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HarnessAgent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;harness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createKiroHarness&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4100&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createUnsafeLocalSandbox&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4100&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;hostBins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;harnessId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kiro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kiro-cli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;kiroExecutable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;permissionMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;allow-reads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Manage files in the fixture project. List files before deleting. Never guess a filename.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;listFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteFile&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;toolApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;deleteFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-approval&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/ErikCH/blog-posts/tree/main/apps/ai-sdk-7-nuxt-agents/server" rel="noopener noreferrer"&gt;complete Kiro route and ACP preset&lt;/a&gt; include session creation and approval continuation. When a response comes back from the browser, the route gathers pending approval responses and calls &lt;code&gt;continueStream()&lt;/code&gt; against the Kiro session.&lt;/p&gt;

&lt;p&gt;My recorded run asked more than once before deleting &lt;code&gt;keep-me.md&lt;/code&gt;. Kiro confirmed the target, its permission flow asked to run the tool, and the host &lt;code&gt;deleteFile&lt;/code&gt; policy asked for the final approval. It was a little repetitive, but it exposed an important boundary. Kiro's built-in permissions and AI SDK's host-tool approval are separate systems.&lt;/p&gt;

&lt;p&gt;The local sandbox in this sample is a development adapter. It limits file API paths to a temporary root, but processes still run as the current operating-system user. Replace it with an isolated sandbox provider before exposing a coding agent to untrusted prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Keep approval in its lane
&lt;/h2&gt;

&lt;p&gt;Tool approval is a product DX improvement, it isn't security.&lt;/p&gt;

&lt;p&gt;If a person approves the wrong path, the tool still removes the wrong path. If the tool can reach the rest of the filesystem, approval does not narrow that access. Keep the path checks from Step 2, apply authorization inside the tool, validate inputs on the server, and use an isolated runtime for agents that can run commands.&lt;/p&gt;

&lt;p&gt;The sample app also keeps its recording controls in development mode. The browser can turn approval off for the first demo, but the built app ignores that flag and requires approval. A client-controlled switch that disables confirmation should not ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;Stop the Nuxt development server. If you deleted either fixture during the demo, recreate both files before your next run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'# Old draft\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fixtures/old-draft.md
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'# Keep me\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fixtures/keep-me.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tutorial does not provision AWS resources. Amazon Bedrock requests can still incur charges, so stop sending test prompts when you finish.&lt;/p&gt;

&lt;p&gt;If you delete the local project directory, its files and any local session state are removed. Copy anything you want to keep before deleting it. AWS credentials loaded through the provider chain remain in their original profile or identity-center cache; this app does not write them into the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finale
&lt;/h2&gt;

&lt;p&gt;I learned a lot by adding this tool approval process. And even though I normally use Strands Agents, the AI SDK 7 worked really well.&lt;/p&gt;

&lt;p&gt;Make sure to leave a comment below if you got this far!&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://vercel.com/blog/ai-sdk-7" rel="noopener noreferrer"&gt;AI SDK 7 announcement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/changelog/ai-sdk-7" rel="noopener noreferrer"&gt;AI SDK 7 changelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sdk.vercel.ai/docs/getting-started/nuxt" rel="noopener noreferrer"&gt;AI SDK Nuxt guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ui.nuxt.com/docs/components/chat" rel="noopener noreferrer"&gt;Nuxt UI chat components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev/docs/cli/acp/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro CLI ACP documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/machine-learning/getting-started-with-cross-region-inference-in-amazon-bedrock/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock cross-Region inference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ErikCH/blog-posts/tree/main/apps/ai-sdk-7-nuxt-agents" rel="noopener noreferrer"&gt;Companion code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nuxt</category>
      <category>ai</category>
      <category>aws</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Nuxt 4.5 SSR Streaming Is Kind Of A Big Deal</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 11 Aug 2026 17:41:30 +0000</pubDate>
      <link>https://dev.to/erikch/nuxt-45-ssr-streaming-is-kind-of-a-big-deal-2i37</link>
      <guid>https://dev.to/erikch/nuxt-45-ssr-streaming-is-kind-of-a-big-deal-2i37</guid>
      <description>&lt;p&gt;&lt;a href="https://nuxt.com/blog/v4-5" rel="noopener noreferrer"&gt;Nuxt 4.5&lt;/a&gt; launched last month and it's really neat. One of my most favorite features is the experimental SSR streaming. With one simple change in the Nuxt config I was able to reduce the Largest Contentful Paint (LCP) by over 2 seconds. I simply enabled the experimental SSR streaming, and the first byte arrived in 16 milliseconds.&lt;/p&gt;

&lt;p&gt;With out it, the server spent about 2.5 seconds rendering the page. Streaming changed what the browser could display during that wait. &lt;/p&gt;

&lt;p&gt;As a part of my testing I also tried out two other features: &lt;code&gt;useLayout()&lt;/code&gt; and named views. Let's take a look at all of these now!&lt;/p&gt;

&lt;p&gt;This post walks through all three features using Nuxt 4.5.2. Let's take a look!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you rather watch check out my full video here&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Bty-hzN8l84"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 22 or another version supported by Nuxt 4.5&lt;/li&gt;
&lt;li&gt;npm&lt;/li&gt;
&lt;li&gt;Basic experience with Nuxt pages, components, and layouts&lt;/li&gt;
&lt;li&gt;Chrome or another browser with document-request timing tools&lt;/li&gt;
&lt;li&gt;My sample app! &lt;a href="https://github.com/ErikCH/nuxt-ssr-streaming" rel="noopener noreferrer"&gt;https://github.com/ErikCH/nuxt-ssr-streaming&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After cloning down the repo, build it and run it in preview.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm ci
npm run typecheck
npm run build
npm run preview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo preview runs on port 3010. The streamed route is &lt;code&gt;/&lt;/code&gt;, and &lt;code&gt;/buffered&lt;/code&gt; uses the same components with streaming disabled by a route rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature 1: Experimental SSR streaming
&lt;/h2&gt;

&lt;p&gt;Normal server-side rendering can buffer the document until every server-rendered component finishes. A slow database query or API request can leave the browser waiting even when the page shell is ready.&lt;/p&gt;

&lt;p&gt;Nuxt 4.5 added the experimental &lt;code&gt;ssrStreaming&lt;/code&gt; option. It uses Vue's web-stream renderer to flush ready HTML while later boundaries continue rendering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a controlled slow component
&lt;/h3&gt;

&lt;p&gt;I used a fixed delay so the delivery difference would be easy to see. The delay runs on the server, and &lt;code&gt;useState&lt;/code&gt; serializes the result into the Nuxt payload for hydration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SlowResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;resolvedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;serverDurationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;renderMarker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;withDefaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;defineProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRoute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SlowResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`slow-panel:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;resolvedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;serverDurationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;renderMarker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;resolvedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;serverDurationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;renderMarker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Place the component inside a Vue &lt;code&gt;Suspense&lt;/code&gt; boundary. The fallback becomes part of the shell that Nuxt can send before &lt;code&gt;SlowPanel&lt;/code&gt; resolves.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;header&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Nuxt 4.5 rendering test&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;The shell is ready&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;Suspense&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;SlowPanel&lt;/span&gt; &lt;span class="na"&gt;:delay-ms=&lt;/span&gt;&lt;span class="s"&gt;"2500"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;#fallback&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;article&lt;/span&gt; &lt;span class="na"&gt;aria-live=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt; &lt;span class="na"&gt;aria-busy=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Shell received&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Waiting for the slow server boundary...&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Suspense&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fallback needs to be useful. Navigation, page context, and a clear loading state give the visitor something to work with. Sending an empty shell earlier does little for the experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enable streaming in production
&lt;/h3&gt;

&lt;p&gt;The demo enables streaming outside development and keeps &lt;code&gt;/buffered&lt;/code&gt; as a control route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ssrStreamingEnabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;compatibilityDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-07-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;devLogs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ssrStreaming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ssrStreamingEnabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;runtimeConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;public&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ssrStreamingEnabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;routeRules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/buffered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;streaming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;/buffered&lt;/code&gt; route uses the same slow component, delay, and shell. Its route rule changes the response mode without changing the work performed by the component.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare the responses
&lt;/h3&gt;

&lt;p&gt;Open the streamed and buffered routes as full document requests. Disable the browser cache and use a hard reload. Client-side navigation does not create a new SSR document request.&lt;/p&gt;

&lt;p&gt;You can also measure the response chunks from a second terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run measure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My local production preview produced this result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;streamed: first byte 16 ms, complete 2562 ms, 14 chunks, 5.8 KB
buffered: first byte 2518 ms, complete 2518 ms, 1 chunk, 5.8 KB
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both routes performed the same delayed work and returned the same amount of HTML. The streamed route delivered the response in 14 chunks and gave the browser useful HTML much earlier. The buffered route delivered one chunk after the slow boundary finished.&lt;/p&gt;

&lt;p&gt;This was just a test, on a somewhat contrived setup. However, I would try this yourself in your own app and see how the results go. Make sure to try it in development first, before going to production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check the production constraints
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ssrStreaming&lt;/code&gt; is experimental and disabled by default. Nuxt can fall back to buffered rendering for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bots and crawlers&lt;/li&gt;
&lt;li&gt;Cached routes&lt;/li&gt;
&lt;li&gt;Incremental Static Regeneration (ISR)&lt;/li&gt;
&lt;li&gt;Stale-while-revalidate (SWR) routes&lt;/li&gt;
&lt;li&gt;Redirects&lt;/li&gt;
&lt;li&gt;Routes using &lt;code&gt;ssr: false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Prerendered output&lt;/li&gt;
&lt;li&gt;Routes with &lt;code&gt;streaming: false&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Streaming also changes when the HTTP response becomes committed. After the shell has been flushed, later component code may be too late to change the status, headers, or cookies. Test authentication redirects, cookie writes, cache headers, errors, and middleware before enabling streaming on a production route.&lt;/p&gt;

&lt;p&gt;I would start with a content-heavy page that has a useful shell and one isolated slow boundary. Measure the deployed route with its real adapter and route rules. Keep buffering where the application needs to finish response decisions before sending HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature 2: Read the resolved layout with &lt;code&gt;useLayout()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Nuxt 4.5 added a stable &lt;code&gt;useLayout()&lt;/code&gt; composable. It returns a read-only computed ref containing the layout Nuxt resolved for the current route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLayout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;output&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"layout-badge"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;useLayout()&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/output&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value accounts for page metadata, route-rule layout selection, and the default layout. It also updates during navigation.&lt;/p&gt;

&lt;p&gt;In the demo, two child pages select different layouts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/pages/features/overview.vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;definePageMeta&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/pages/features/focus.vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;definePageMeta&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;focus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parent page stays mounted while navigation changes the child route. The computed layout updates from &lt;code&gt;default&lt;/code&gt; to &lt;code&gt;focus&lt;/code&gt; without requiring the parent to inspect route metadata itself.&lt;/p&gt;

&lt;p&gt;Reading &lt;code&gt;route.meta.layout&lt;/code&gt; can miss layouts selected elsewhere in Nuxt's resolution chain. Use &lt;code&gt;useLayout()&lt;/code&gt; when a component needs the layout Nuxt is actually rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature 3: Render multiple outlets with named views
&lt;/h2&gt;

&lt;p&gt;Named views let one route render into multiple page outlets. Nuxt 4.5 uses a &lt;code&gt;name@view.vue&lt;/code&gt; filename convention for the additional view files.&lt;/p&gt;

&lt;p&gt;The demo uses this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/pages/
  features.vue
  features/
    index.vue
    overview.vue
    overview@sidebar.vue
    focus.vue
    focus@sidebar.vue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parent page creates a default outlet and a named sidebar outlet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Named view outlets"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;article&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;NuxtPage&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;aside&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;NuxtPage&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"sidebar"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you visit &lt;code&gt;/features/overview&lt;/code&gt;, &lt;code&gt;overview.vue&lt;/code&gt; fills the default outlet and &lt;code&gt;overview@sidebar.vue&lt;/code&gt; fills the sidebar. The &lt;code&gt;focus&lt;/code&gt; files do the same work for &lt;code&gt;/features/focus&lt;/code&gt;. Both outlets belong to one URL, and the parent does not need a manual component map.&lt;/p&gt;

&lt;p&gt;Keep route metadata in the default page file. Nuxt ignores &lt;code&gt;definePageMeta&lt;/code&gt; in an &lt;code&gt;@sidebar&lt;/code&gt; file. If a route has no matching named-view file, that outlet remains empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finale
&lt;/h2&gt;

&lt;p&gt;I am really impressed by the incremental changes in Nuxt 4.5. SSR streaming is something I'll be using on all my future apps. And useLayout and named views are nice DX bonuses. &lt;/p&gt;

&lt;p&gt;Leave a comment and let me know which of these features you'll be using next!&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nuxt.com/blog/v4-5" rel="noopener noreferrer"&gt;Nuxt 4.5 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nuxt.com/docs/4.x/guide/going-further/experimental-features" rel="noopener noreferrer"&gt;Nuxt experimental features&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nuxt.com/docs/4.x/api/composables/use-layout" rel="noopener noreferrer"&gt;Nuxt &lt;code&gt;useLayout()&lt;/code&gt; documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nuxt.com/docs/4.x/guide/directory-structure/app/pages#named-views" rel="noopener noreferrer"&gt;Nuxt named views documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vuejs.org/api/ssr.html#rendertowebstream" rel="noopener noreferrer"&gt;Vue server-rendering API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=Bty-hzN8l84" rel="noopener noreferrer"&gt;Watch the full Nuxt 4.5 video&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nuxt</category>
      <category>vue</category>
      <category>ssr</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How To learn Web Development in 2026</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Mon, 03 Aug 2026 17:19:26 +0000</pubDate>
      <link>https://dev.to/erikch/how-to-learn-web-development-in-2026-52j5</link>
      <guid>https://dev.to/erikch/how-to-learn-web-development-in-2026-52j5</guid>
      <description>&lt;p&gt;Learning web development in 2026 is a lot different than it was when I first learned in 2010. Back then, I used Udemy courses, Stack Overflow, and official documentation. I’d spend hours copying and pasting code, only to discover that the blog post I was pulling it from was out of date. It was frustrating, to say the least.&lt;/p&gt;

&lt;p&gt;Luckily, learning a new framework in 2026 is a lot easier than it was. &lt;/p&gt;

&lt;p&gt;Recently I was at Chain React. It's a React Native conference and for &lt;a href="https://chainreactconf.com/talks/how-i-learned-react-native-with-ai" rel="noopener noreferrer"&gt;my talk&lt;/a&gt; I decided to really deep into the framework and learn it. I started doing the same thing I always did, reading the docs, buying courses etc. Then I realized I could use AI to speed up my learning. This is what I want to teach you today, with a few additional tips along the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want to watch instead, check out my full video on the subject!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/2WIlokBwlzY"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;(Full disclosure before I start naming tools: I'm a Developer Advocate at AWS, and &lt;a href="https://kiro.dev/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; is an AWS product. It's the agentic harness I use daily. Everything here works the same in Claude Code, Cursor, or whatever you already have open.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a goal, not a curriculum
&lt;/h2&gt;

&lt;p&gt;I don't start by learning a framework. I start with something that I want to build for fun.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo4bboqg3ouoms52chq52.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo4bboqg3ouoms52chq52.png" alt="thinking slide" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I was learning React Native, it was a workout tracker. I wanted to learn a program called 75 Hard, which is 75 days of hitting the same set of daily goals including a diet, an outdoor workout, and a second workout. I wanted to check those off on my phone. That was the goal.&lt;/p&gt;

&lt;p&gt;Making sure you have a goal is really important. A todo list app teaches you nothing, because it means almost nothing. A real app forces decisions you have to make, like does it work offline, where does the data live, what happens when permissions get denied. Pick something with a little resistance in it, ideally tied to a hobby so you'll still care as your learning progression continues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide how much you actually need to know
&lt;/h2&gt;

&lt;p&gt;In other words, how much of it do I need to actually learn to achieve my goal?&lt;/p&gt;

&lt;p&gt;In today's age, you need to learn the primitives. The basics, and some architecture of how it all works. You do not need to memorize API signatures anymore.&lt;/p&gt;

&lt;p&gt;Let's imagine you are learning React. If you're using &lt;code&gt;useState&lt;/code&gt; everywhere, understand what it does and why re-renders happen. Whether you can recall the exact argument order from memory is irrelevant, because the model will write it. Same with optional chaining, same with whatever config format the build tool wants this year. Learn the basics of the framework or library you are learning. Let the agent handle the syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask three models what they'd build today
&lt;/h2&gt;

&lt;p&gt;Before I learn anything, I want to know what the ecosystem actually looks like right now. So I ask several models the same broad question and compare.&lt;/p&gt;

&lt;p&gt;When I was learning React Native, I ran the same prompt through Gemini, Claude, and GPT. Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build a mobile application for iOS.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That comes back with Swift, which tells me something. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build a cross-platform iOS and Android application.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I get Flutter or React Native. I'm not asking for code here. I'm reading the consensus from the agent to find out which frameworks and libraries it's recommending, which tells me whether the thing I'm about to learn is the thing I should be learning.&lt;/p&gt;

&lt;p&gt;When the models disagree, that's when I dig in further to find out why. I'll usually google around a bit to see if I'm on the right path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get a personalized learning path
&lt;/h2&gt;

&lt;p&gt;Finally, I work on a personalized learning path, based on what I know already. Here is an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build me a curriculum to learn React Native. Assume I already
have basic knowledge of React, HTML, CSS, and JavaScript.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second sentence is where I brought in my own personalization. I know web development, so I wanted to make sure my learning path is tailored to me. Also, I called out React Native, as per the last section, I learned it's the most popular and makes the most sense for me.&lt;/p&gt;

&lt;p&gt;Because I told it I already knew web basics, it skips React fundamentals and goes straight to what's different: native components instead of the DOM, &lt;code&gt;StyleSheet&lt;/code&gt; instead of the CSS cascade, navigation as a stack instead of URLs, you get the idea. I then get topics with a suggested time frame, and I can work through them at my own pace.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; model selection matters here more than almost anywhere else. I ran this against Sonnet 4 and Opus 5. Sonnet 4 handed me what was essentially 2023 React Native. Opus 5 gave me current information and a noticeably better curriculum. Every model has a different training cutoff, and a stale curriculum is worse than no curriculum, because you don't know which parts are wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're stuck on an older or a local model, run an adversarial review. Have the cheap model draft the learning path, then hand it to a current model and ask what's out of date. Going past two or three models surfaces a surprising amount, and it's also a good idea when you are trying to save tokens.&lt;/p&gt;

&lt;p&gt;Also make sure you use some of the tools your harness gives you. Most harnesses have search built in, plus MCP servers and skills. If you're learning React, install the current React skills before you generate anything. Scaffolding tools help here too: &lt;code&gt;create-next-app&lt;/code&gt; now drops an &lt;code&gt;AGENTS.md&lt;/code&gt; in your project, and that file is a better starting point for your curriculum than the model's memory. &lt;/p&gt;

&lt;h2&gt;
  
  
  Build it: I do, we do, you do
&lt;/h2&gt;

&lt;p&gt;A learning path is a good start. It's still reading. To move to actual understanding I used an old teaching framework from 1983.&lt;/p&gt;

&lt;p&gt;It's called gradual release of responsibility, from a paper by Pearson and Gallagher, and it sits on top of Vygotsky's zone of proximal development. The sequence is &lt;strong&gt;I do, we do, you do&lt;/strong&gt;. The teacher demonstrates, then you work together, then you work alone.&lt;/p&gt;

&lt;p&gt;To use this framework today, the &lt;strong&gt;the AI becomes the "I."&lt;/strong&gt; It does, you watch. Then you work together with the AI, and finally you do it yourself. It's really helped me learn. Let's start with ** I do **&lt;/p&gt;

&lt;h3&gt;
  
  
  I do: let it build, then read it
&lt;/h3&gt;

&lt;p&gt;Let the agent build the whole thing. Just create a simple prompt, and let it do it's thing. Don't type anything. Read the output, look at the file structure, see how it wired things together.&lt;/p&gt;

&lt;p&gt;You are not learning yet, and that's fine. This phase is the right one because it gives you an idea what is possible. If you like you could look over the code that was written, but it's not the best way to learn.&lt;/p&gt;

&lt;h3&gt;
  
  
  We do: spec-driven development
&lt;/h3&gt;

&lt;p&gt;This is where most of the learning actually happened for me.&lt;/p&gt;

&lt;p&gt;Spec-driven development means writing structured specifications first, so the agent can build and verify against them. In practice I ask for a spec instead of an app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a spec that helps build a retro workout planner app.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favz2px260gllq0i7yxhm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favz2px260gllq0i7yxhm.png" alt="wedo graphic" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What comes back is a design document, and the design document is the cheat sheet. Mine told me to use Expo Router 57, &lt;code&gt;expo-sqlite&lt;/code&gt;, an image picker, and notifications. That's a map of the architecture and the current library choices for a framework I didn't know yet. When I was working like this, I would constantly ask clarifying questions and ask why certain decisions were made. I would even often ask for changes.&lt;/p&gt;

&lt;p&gt;Then it generates requirements, usually as user stories. I skip this one when I'm learning. It's more useful when you're shipping to other people.&lt;/p&gt;

&lt;p&gt;The implementation plan is the part you need to pay attention to. It's a task list. &lt;strong&gt;Instead of letting the agent execute it, I work through the tasks myself, with the design doc open.&lt;/strong&gt; When I get stuck, I say so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I tried to install NativeWind 5 and I don't understand what to do here. Can you help?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffbi8yu5j856qvl0efjzs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffbi8yu5j856qvl0efjzs.png" alt="implementation plan" width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I get help, and I might even read the official documentation anyways. That's the "we do" phase working exactly as designed. I have enough context to attempt it and a patient buddy for the gaps. It's almost like &lt;a href="https://en.wikipedia.org/wiki/Rubber_duck_debugging" rel="noopener noreferrer"&gt;rubber ducking&lt;/a&gt;, but with something that can respond.&lt;/p&gt;

&lt;p&gt;Keep in mind, if you write a spec and then let the agent implement all of it, you haven't learned much. You can now explain what your app does and still not explain how the framework does it. So I settled on a rule. &lt;strong&gt;Spec the what, hand-write the how, at least once per concept.&lt;/strong&gt; Write the spec for the app, but make sure your writing things yourself (with help if needed). &lt;/p&gt;

&lt;h3&gt;
  
  
  You do: turn it off
&lt;/h3&gt;

&lt;p&gt;Close the tab. Build something small with no assistance.&lt;/p&gt;

&lt;p&gt;Don't skip this step. This is the best way to check your recall and that you really understand everything. I wouldn't write the whole thing from scratch, mind you, but a few pieces just to make sure I understand the underlying concepts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more patterns worth stealing
&lt;/h2&gt;

&lt;p&gt;Let the AI quiz you. Take the learning path you already generated and ask for a quiz on it, then go back and forth. It's a quick way to find out which parts you only think you know.&lt;/p&gt;

&lt;p&gt;The stronger version of this flips the direction. Don't ask the model to explain hooks to you. Explain hooks &lt;em&gt;to the model&lt;/em&gt; and ask it to grade you. "Here's my understanding of the New Architecture, what did I get wrong?" Use text-to-speach if you can, to make this even quicker.&lt;/p&gt;

&lt;p&gt;The other pattern is learning in public, which I was doing long before agents existed. Post what you're learning, on YouTube or Bluesky or a blog. Writing it down for someone else is what exposes the parts you weren't sure on. This post exists because I told a conference I'd stand up and explain React Native, and that deadline taught me more than any course.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I've landed
&lt;/h2&gt;

&lt;p&gt;Start with a goal you care about. Use your usual harness on a current model. Have it build you a plan, with search and MCP servers turned on so it isn't working from memory. Learn the primitives and the architecture, skip memorizing the APIs. Then try out something like: having the AI build it, build alongside it, then build alone.&lt;/p&gt;

&lt;p&gt;AI didn't replace the learning. It replaced the searching. All those hours of hunting for the right Stack Overflow answer are gone, and I don't miss them. But the part where you sit with something confusing until it stops being confusing? That still has to happen. There's just no tab for it.&lt;/p&gt;

&lt;p&gt;How do you learn a new framework these days? Let me know in the comments if you do it differently. Until next time.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>learning</category>
      <category>beginners</category>
    </item>
    <item>
      <title>7 Kiro Features You're Probably Not Using</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Mon, 27 Jul 2026 15:39:49 +0000</pubDate>
      <link>https://dev.to/aws/7-kiro-features-youre-probably-not-using-2417</link>
      <guid>https://dev.to/aws/7-kiro-features-youre-probably-not-using-2417</guid>
      <description>&lt;p&gt;Did you know that &lt;a href="https://kiro.dev/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; doesn't just have a Spec-Driven Development (SDD) flow, but also a bug fix workflow that helps you resolve one issue at a time? That's one of seven features worth knowing about.&lt;/p&gt;

&lt;p&gt;If you're completely new to Kiro, it's an agentic harness for the CLI, web, IDE, iOS, and more. It helps teams and individuals do their best work while coding. I've been using it since it launched in July last year, and I keep finding features I didn't know were there.&lt;/p&gt;

&lt;p&gt;(Full disclosure: I'm a Developer Advocate at AWS, and Kiro is a part of AWS. I use it every day, and I'll be forthcoming about the parts that are still preview or experimental.)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; Kiro ships fast. I've flagged the version-sensitive bits of these features inline. Check the &lt;a href="https://kiro.dev/docs/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;docs&lt;/a&gt; if something looks different in your build.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Stop approving every single command
&lt;/h2&gt;

&lt;p&gt;After talking to a lot of people about Kiro, one of the main pieces of feedback I get is on approving commands. When Kiro asks permission to run a shell command, the default reaction is to hit yes and move on. Then it asks again for the next &lt;code&gt;git&lt;/code&gt; command. And the next one.&lt;/p&gt;

&lt;p&gt;Press &lt;strong&gt;Tab&lt;/strong&gt; instead in the CLI. This allows you to edit it and put the exact permissions you'd like. For example you can be pickier on the trust tiers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git pull &lt;span class="nt"&gt;--rebase&lt;/span&gt;     &lt;span class="c"&gt;# this exact command&lt;/span&gt;
git pull &lt;span class="k"&gt;*&lt;/span&gt;            &lt;span class="c"&gt;# git pull with any arguments&lt;/span&gt;
git &lt;span class="k"&gt;*&lt;/span&gt;                 &lt;span class="c"&gt;# anything git&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt;                     &lt;span class="c"&gt;# the entire shell tool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whatever you pick persists for the session and gets stored as a regex in your agent's &lt;code&gt;allowedCommands&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There's also &lt;code&gt;/tools trust-all&lt;/code&gt;, which trusts everything. It's the documented replacement for the old &lt;code&gt;/acceptall&lt;/code&gt;, and the &lt;a href="https://kiro.dev/docs/cli/chat/security/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;security docs&lt;/a&gt; are blunt about it: don't use it in production or with sensitive data, and you're responsible for whatever it does. &lt;/p&gt;

&lt;p&gt;One version note: on CLI v3 this moves to a &lt;code&gt;permissions.yaml&lt;/code&gt; file, so the agent JSON advice above is v2. More on v3 in a minute.&lt;/p&gt;

&lt;p&gt;Full details: &lt;a href="https://kiro.dev/docs/cli/chat/permissions/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;tool permissions&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The &lt;code&gt;#&lt;/code&gt; menu is bigger than &lt;code&gt;#file&lt;/code&gt; in the IDE
&lt;/h2&gt;

&lt;p&gt;Type &lt;code&gt;#&lt;/code&gt; in the IDE chat and you get a list of context providers. &lt;code&gt;#file&lt;/code&gt; is the one I use a lot. The full list:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#codebase&lt;/code&gt; · &lt;code&gt;#file&lt;/code&gt; · &lt;code&gt;#folder&lt;/code&gt; · &lt;code&gt;#git diff&lt;/code&gt; · &lt;code&gt;#terminal&lt;/code&gt; · &lt;code&gt;#problems&lt;/code&gt; · &lt;code&gt;#url:&lt;/code&gt; · &lt;code&gt;#code:&lt;/code&gt; · &lt;code&gt;#repository&lt;/code&gt; · &lt;code&gt;#current&lt;/code&gt; · &lt;code&gt;#steering:&lt;/code&gt; · &lt;code&gt;#docs:&lt;/code&gt; · &lt;code&gt;#spec:&lt;/code&gt; · &lt;code&gt;#mcp:&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You can stack them in a single message. &lt;code&gt;#git diff #problems review my changes&lt;/code&gt; is a different request than either one alone.&lt;/p&gt;

&lt;p&gt;The one I've been using more is the &lt;code&gt;#terminal&lt;/code&gt;. It hands Kiro your recent terminal output and command history, so you stop copying and pasting errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;terminal why did this build fail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing I find handy is the "#Currently Open files." However, keep in mind that Kiro pulls in your open files and their dependencies automatically without you asking.&lt;/p&gt;

&lt;p&gt;Full list: &lt;a href="https://kiro.dev/docs/chat/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el#context-providers" rel="noopener noreferrer"&gt;context providers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also you can't drag a file in the IDE's own explorer to the chat window, however you can copy and past the location of the file and past that into the chat window as a workaround. &lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pick a workflow instead of freeform chatting
&lt;/h2&gt;

&lt;p&gt;Kiro gives you structured starting points, and it's tempting to just start typing into the chat box instead. Instead you can choose any of these in the IDE. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc5jfyheu0l5gd8ftqp2m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc5jfyheu0l5gd8ftqp2m.png" alt="IDE chooser" width="800" height="749"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spec&lt;/strong&gt; for a full feature, with requirements, design, and tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan&lt;/strong&gt; when you want an implementation plan and zero code changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bug Fix&lt;/strong&gt; to investigate, diagnose, and resolve one problem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick Spec&lt;/strong&gt; when you want Kiro to ask a couple of clarifying questions and then generate the requirements, design, and tasks for you&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bug Fix is the one I'd try first. It writes down current behavior, expected behavior, and unchanged behavior. I like how it doesn't change a bunch of files it shouldn't. It basically tells the agent what it is not allowed to touch. &lt;/p&gt;

&lt;p&gt;On a feature spec you also get a choice between requirements-first and tech design-first. If you already know your architecture, pick design-first. There's no reason to answer requirements questions you already have answers to.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Skills are slash commands
&lt;/h2&gt;

&lt;p&gt;Kiro activates a &lt;a href="https://kiro.dev/docs/skills/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;skill&lt;/a&gt; automatically when your prompt matches the skill's description. That works, but I feel like it doesn't always automatically activate.&lt;/p&gt;

&lt;p&gt;You can invoke one directly instead. Skills in &lt;code&gt;.kiro/skills/&lt;/code&gt; or &lt;code&gt;~/.kiro/skills/&lt;/code&gt; show up as slash commands, so a skill named &lt;code&gt;pr-review&lt;/code&gt; becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/pr-review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That loads the entire instruction file, not just the description. Works in the CLI and the IDE.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kiro.dev/docs/powers/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Powers&lt;/a&gt; are a different thing and worth knowing about separately. A power bundles MCP server config with steering and optional hooks, and Kiro loads it only when your task is relevant to it. Powers are keyword-activated, and they're free on every plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. You might be running an old build
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;Help &amp;gt; About&lt;/code&gt; and check your version against &lt;a href="https://kiro.dev/downloads/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;kiro.dev/downloads&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Kiro's own changelog says auto-updates were paused for IDE 1.0.x and tells you to download the latest release directly from the site. So if your version doesn't match what's on the downloads page, that's why, and no amount of restarting will fix it. Reinstalling keeps your settings, extensions, and sign-in state.&lt;/p&gt;

&lt;p&gt;On the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kiro-cli &lt;span class="nt"&gt;--version&lt;/span&gt;
kiro-cli update
kiro-cli doctor      &lt;span class="c"&gt;# install and config problems&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;/changelog&lt;/code&gt; in the CLI inside a chat session shows the release notes inline, which is a nicer way to find out what shipped than scrolling a webpage.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The work doesn't have to run on your laptop
&lt;/h2&gt;

&lt;p&gt;One subscription covers the IDE, the CLI, &lt;a href="https://kiro.dev/docs/web/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro on the web&lt;/a&gt;, ACP-compatible editors, and automation in CI/CD. Same credit pool.&lt;/p&gt;

&lt;p&gt;I really like the web version. You kick off a session, it runs in a cloud sandbox, and it opens the pull request when it's done. Your laptop can be closed. There are also Automations, which are cron-scheduled agent runs that open PRs on their own.&lt;/p&gt;

&lt;p&gt;Web is still in preview, gated to Pro and above, and it needs a connected GitHub account. There's an iOS app too, but it's early access via TestFlight, Pro or higher, and it's a surface for starting and reviewing cloud sessions rather than an editor. No Android yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The experimental stuff is opt-in and worth trying
&lt;/h2&gt;

&lt;p&gt;Three of these are behind a flag or a toggle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI v3.&lt;/strong&gt; Run &lt;code&gt;kiro-cli --v3&lt;/code&gt; and it runs alongside your existing 2.x setup without touching it. You get the spec agent in the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kiro-cli &lt;span class="nt"&gt;--v3&lt;/span&gt;
/spec new my-feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus capability-based &lt;code&gt;permissions.yaml&lt;/code&gt;, standalone hooks in &lt;code&gt;.kiro/hooks/*.json&lt;/code&gt;, and markdown agent configs. See the &lt;a href="https://kiro.dev/docs/cli/v3/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;v3 docs&lt;/a&gt; for the full list of features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent Focus in the IDE.&lt;/strong&gt; Toggle it from the button in the top right. The layout flips: parallel sessions down the left, chat in the middle, specs and diffs in a panel on the right. It's a different job than editing files. You're directing several agents and reviewing what they produce. Settings, powers, MCP management, the terminal, and direct file editing all still live in the normal view, so you'll toggle back and forth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;/goal&lt;/code&gt;.&lt;/strong&gt; Give it an objective and a definition of done, and it loops implement-then-verify until the criteria are met:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/goal Migrate the auth module to the new SDK. Done when all tests pass and there are no TypeScript errors.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is available in v3 CLI.&lt;/p&gt;

&lt;p&gt;Default is five iterations, &lt;code&gt;--max &amp;lt;n&amp;gt;&lt;/code&gt; raises it, &lt;code&gt;/goal clear&lt;/code&gt; cancels. The docs are right that the definition of done is the part that matters. "All tests pass" works. "Make it better" does not. You can interrupt mid-loop, or nudge it without cancelling using queue steering with &lt;code&gt;Ctrl+S&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few more worth a mention
&lt;/h2&gt;

&lt;p&gt;While I'm listing CLI commands: &lt;code&gt;/rewind&lt;/code&gt; forks the conversation at an earlier turn instead of you fighting a thread that went off the rails, and the original session is preserved. &lt;code&gt;/compact&lt;/code&gt; summarizes history to free up context. &lt;code&gt;/tangent&lt;/code&gt; (&lt;code&gt;Ctrl+T&lt;/code&gt;) lets you run commands on the side (like /btw in Claude). &lt;code&gt;/guide&lt;/code&gt; is a docs-grounded agent for "how do I do X in Kiro" questions, and it can write agents, prompts, and steering files into &lt;code&gt;.kiro/&lt;/code&gt; for you.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;Shift+Enter&lt;/code&gt; for multi-line input has been annoying you, &lt;code&gt;/settings terminal&lt;/code&gt; fixes it and backs up your terminal config first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go when you're stuck
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://kiro.dev/discord/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro Discord&lt;/a&gt; is the main support channel, and there's an active subreddit at r/kiroIDE. Note the name, it's kiroIDE, not r/kiro. Billing lives in your account settings. Bugs and feature requests go to the &lt;a href="https://github.com/kirodotdev/Kiro/issues/new/choose" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;, and &lt;code&gt;/issue&lt;/code&gt; in the CLI opens that workflow for you. If you need to send logs along with a bug report, &lt;code&gt;/logdump&lt;/code&gt; zips them up.&lt;/p&gt;

&lt;p&gt;Which of these did you not know about? Let me know in the comments. Until next time.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Nuxt vs SvelteKit: What works better?</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Wed, 22 Jul 2026 01:13:24 +0000</pubDate>
      <link>https://dev.to/erikch/nuxt-vs-sveltekit-what-works-better-132h</link>
      <guid>https://dev.to/erikch/nuxt-vs-sveltekit-what-works-better-132h</guid>
      <description>&lt;p&gt;Nuxt vs SvelteKit. Which one is better?&lt;/p&gt;

&lt;p&gt;That is is what I've been testing out this week. I built the same app twice. Once in Nuxt with the version 5 compatibility preview turned on, and once in SvelteKit using its experimental remote functions.&lt;/p&gt;

&lt;p&gt;I created a basic task app. Where you can add or remove tasks. I also logged the network requests and timing. The biggest thing I noticed was that the SvelteKit app would make one request, when creating a new task, while the Nuxt app needed two.&lt;/p&gt;

&lt;p&gt;That one difference turned out to be the most interesting part of the whole comparison, so let's jump in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Both apps are a simple task list backed by an in browser memory store. Both are production builds running locally, and both render the initial task list on the server.&lt;/p&gt;

&lt;p&gt;Quick caveat, Nuxt 5 is not released yet. My Nuxt app is stable Nuxt 4.5 with the compatibility flag set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// nuxt.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;compatibilityDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-07-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;future&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;compatibilityVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And SvelteKit's remote functions are still marked experimental in the docs. So this is a comparison of directions, not finished products.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;When I add a task in the Nuxt app, I get a POST to &lt;code&gt;/api/tasks&lt;/code&gt; (about 690 ms) followed by a GET to &lt;code&gt;/api/tasks&lt;/code&gt; (about 450 ms) to refresh the list. A little over 1,100 ms total, and the timeline panel reports two browser requests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbv3bxswcuz5iy702h5ns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbv3bxswcuz5iy702h5ns.png" alt="Nuxt request" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I add a task in the SvelteKit app, I get one request. About 1,100 ms. The server still does both operations, the mutation and the read, but they come back in a single response.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fur0jeepdlow1acyb8w7d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fur0jeepdlow1acyb8w7d.png" alt="Svelte Request" width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Plain refreshes were nearly identical. 447 ms in Nuxt, 448 ms in SvelteKit. I ran this quite a few times, and if I had to pick, SvelteKit felt slightly faster overall. But the totals were close enough that I wouldn't choose a framework based on them.&lt;/p&gt;

&lt;p&gt;Let's talk about how requests work in each. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Nuxt version: explicit API routes
&lt;/h2&gt;

&lt;p&gt;If you've used Nuxt before, this will feel familiar. I have two handlers in &lt;code&gt;server/api&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/api/tasks.get.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineEventHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readTasks&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/task-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;readTasks&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The POST handler validates the title and saves the task. These are normal HTTP endpoints. Anything that speaks HTTP can call them.&lt;/p&gt;

&lt;p&gt;On the page, &lt;code&gt;useFetch&lt;/code&gt; loads the initial data during SSR, so hydration doesn't fetch it again. When I add a task, I post with &lt;code&gt;$fetch&lt;/code&gt; and then call &lt;code&gt;refresh()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refresh&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;useFetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TaskSnapshot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task-dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submitTask&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code is explicit, and the network tab matches the code exactly. POST first, GET second.&lt;/p&gt;

&lt;p&gt;Could I avoid the second request? Sure. The POST could return the updated list and I could patch local state myself. I wrote it this way because invalidate-and-refetch is the workflow most of us reach for, and it's exactly the pattern SvelteKit's remote functions are designed to improve. I also find by adding a GET request we are verifying the exact output after the POST.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SvelteKit version: remote functions
&lt;/h2&gt;

&lt;p&gt;This is the experimental feature. You turn it on in &lt;code&gt;svelte.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;kit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;remoteFunctions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;compilerOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you create a file ending in &lt;code&gt;.remote.ts&lt;/code&gt; and export your server functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tasks.remote.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$app/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addTask&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;addTaskToStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readTasks&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$lib/server/task-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;valibot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;taskInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getTasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;readTasks&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addTaskToStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// This runs on the server, and the refreshed query value&lt;/span&gt;
  &lt;span class="c1"&gt;// comes back in the same command response.&lt;/span&gt;
  &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;getTasks&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function bodies always run on the server. In the browser, they become typed wrappers around endpoints SvelteKit generates for you. I really enjoy how there is no public endpoint to hit, it's isolated. It's created for the call, which means I can use environment variables and secrets in there without thinking about it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;void getTasks().refresh()&lt;/code&gt; line is the API on the server side to trigger the refresh. After the mutation, SvelteKit refreshes the query on the server and packages the new value into the command's response. That's the single-flight mutation, and it's why the network tab shows one request instead of two.&lt;/p&gt;

&lt;p&gt;On the page, I just import and call the functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getTasks&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./tasks.remote&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTasks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submitTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SubmitEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;render&lt;/span&gt; &lt;span class="nf"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;await tasks&lt;/code&gt; at the bottom works almost like a subscription. When the server-side refresh happens, the task list updates automatically. No server routes to worry about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F24q7zjeyi2ru8skt8l6t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F24q7zjeyi2ru8skt8l6t.png" alt="remotefunctions" width="799" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My first look at this pattern, I thought it was a little complicated. But it clicked pretty fast, and the query-refreshes-inside-the-command idea makes sense once you see it in the network tab. If you've used server actions in Next or TanStack Start, this will feel like family. I'd say I still like TanStack Start's server actions a bit better, but this is close.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up&lt;/strong&gt;: remote functions have been available since SvelteKit 2.27 and they are still experimental. The API has changed several times over the past few months. If you adopt them early, pin your versions and budget time for migrations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What does Nuxt 5 offer?
&lt;/h2&gt;

&lt;p&gt;Nuxt 5 doesn't have an answer to remote functions. Most of the confirmed work is in the underlying structure. A new version of Nitro, a new Vite integration, and framework internals. The &lt;a href="https://nuxt.com/docs/getting-started/upgrade" rel="noopener noreferrer"&gt;upgrade guide&lt;/a&gt; walks through what to expect, and it's mostly foundation work rather than new application-level APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  My verdict
&lt;/h2&gt;

&lt;p&gt;I'm sticking with Nuxt. I love the API routes pattern, I love Vue, and nothing in this demo is a reason to rewrite an existing app. You can return updated data from a mutation today and skip the second request yourself if it matters.&lt;/p&gt;

&lt;p&gt;But I do miss server actions. Frameworks like SvelteKit, Next, and TanStack Start all have some version of a typed, non-public server function you can call from a component. I hope Nuxt adds something like it in a future update, beyond the server components they have now.&lt;/p&gt;

&lt;p&gt;If you're starting a SvelteKit project and can live with an experimental API, try remote functions first. The single-flight mutation is awesome, and the types crossing the boundary for free is a great developer experience.&lt;/p&gt;

&lt;p&gt;Which side are you on: explicit API routes, or remote functions that generate the transport for you? Let me know in the comments if you agree or disagree. &lt;/p&gt;

&lt;p&gt;BTW, I used &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; for all my research for this post and video! Check it out , it's an amazing harness!&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>sveltekit</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I Used Spec-Driven Development to Build a Game (TanStack Start)</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 14 Jul 2026 22:26:53 +0000</pubDate>
      <link>https://dev.to/erikch/how-i-used-spec-driven-development-to-build-a-game-a5p</link>
      <guid>https://dev.to/erikch/how-i-used-spec-driven-development-to-build-a-game-a5p</guid>
      <description>&lt;p&gt;I've been deep in spec-driven development (SDD) lately, and I want to walk through what it actually is, then show you how I used it to build a &lt;a href="http://programwitherik.com/kirogame" rel="noopener noreferrer"&gt;game&lt;/a&gt; for &lt;a href="https://kiro.dev/birthday/" rel="noopener noreferrer"&gt;Kiro's&lt;/a&gt; first birthday.&lt;/p&gt;

&lt;p&gt;If you haven't run into spec-driven development, or SDD, don't worry, I got you. We'll cover what it is, where it helps, where it's overkill, and how I used it to make this game!&lt;/p&gt;

&lt;p&gt;Would you like to watch a video instead? Check it out!&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/_D-QDJAXMKQ"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What is spec-driven development?
&lt;/h2&gt;

&lt;p&gt;SDD breaks down into three phases, and all of this happens before any code gets written.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz890nfmi2bpt62xr5et5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz890nfmi2bpt62xr5et5.png" alt="SDD phases" width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First you create a requirements document. Then you have your AI editor turn that into a design document. Then, from the requirements and the design, you generate a list of tasks. &lt;/p&gt;

&lt;p&gt;The most important part is that there's a human in the middle at every step. You check the requirements. You check the design. You review and edit the tasks that get created. Nothing gets built until you've signed off on the plan.&lt;/p&gt;

&lt;p&gt;This isn't set in stone, though. Sometimes the requirements and design get combined into a single document. Other times you run it in a quick mode where all three happen at once. It depends on what you're trying to do. When I work on big features I create all three documents, when I'm trying to do something quick, like a demo, I might just use some sort of quick or plan mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-offs
&lt;/h2&gt;

&lt;p&gt;Like anything else, SDD comes with trade-offs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffke4na5ou12lwuyk4vmw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffke4na5ou12lwuyk4vmw.png" alt="SDD Tradeoffs" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The thing I like most is the high first-pass success rate. If you actually do your job up front, writing a real requirements doc and a real design, the end product is a lot more likely to be what you wanted.&lt;/p&gt;

&lt;p&gt;It's also easier to review. You're breaking a big, complex feature into smaller steps, so you can check each one instead, and push up multiple smaller PRs. And it helps prevent you from going in the wrong direction. Instead of letting the agent run off in a bunch of directions and finding out way too late, you're checking the code after every step or two. &lt;/p&gt;

&lt;p&gt;The downside is the up front planning time. SDD takes more time before you see any code, so it's not meant for every task. For throwaway code or a small issue, it's not needed. On the other hand any features that takes more thought it does really well. You give up a little time at the start, and in return you get a more stable, better working app at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The project: a Kiro birthday game
&lt;/h2&gt;

&lt;p&gt;Let me show you what I built.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9l71pzxpg2iwaxuy63sd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9l71pzxpg2iwaxuy63sd.png" alt="Game image" width="800" height="647"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's a &lt;a href="http://programwitherik.com/kirogame" rel="noopener noreferrer"&gt;rogue-like game&lt;/a&gt; where you walk around and explore a bunch of buildings. Each building represents a different point in time for Kiro, because this is part of our Kiro birthday celebration. Kiro is turning one. So one building covers when Kiro went generally available, another is the house we had at re:Invent, and so on. Visit all the houses and you unlock a bonus house/section. I thought it would be a fun way to tell Kiro's history.&lt;/p&gt;

&lt;p&gt;Under the hood it's Phaser and TanStack Start. Phaser is the gaming library doing the 2D Final Fantasy style look, and TanStack Start is the web library wrapping it all. There's music in there too, which I'll get to.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I used SDD to build it
&lt;/h2&gt;

&lt;p&gt;I opened up Kiro and told it to use spec mode. You can do this right in the IDE, and as of v3 of the CLI you can use spec mode there as well. Either way, it creates those three documents: requirements, design, and tasks.&lt;/p&gt;

&lt;p&gt;I gave it a prompt describing the game. Something like, I'm creating this 2D video game, Final Fantasy style, here are the different classes and names, here are the different buildings we're going to have. From that it generated the requirements in EARS format, which is the classic structure of a user story plus acceptance criteria. As I mentioned earlier, I double checked it at every stage to make sure the docs it created were correct.&lt;/p&gt;

&lt;p&gt;From the requirements it built a design. You can skip this part if you'd rather keep everything in one doc, but I like having it. The design got into the technical details. Phaser as the TypeScript library for the front end, TanStack Start with useEffect all over the place, the high-level structure, the scenes, the boundaries, the game shell. &lt;/p&gt;

&lt;p&gt;Then came the task list, with every single task broken out. I told it to build an MVP first, so it took the full list and pulled the four most important tasks to the front. I went in and made sure that ordering made sense, had it implement those first four, looked at the output, and confirmed it held up before moving on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools that made it easier
&lt;/h2&gt;

&lt;p&gt;A few things helped a lot here.&lt;/p&gt;

&lt;p&gt;I used a couple of MCP servers. Context7 to grab the Phaser documentation, so the agent wasn't guessing at the API. And the Chrome browser MCP extension, so Kiro could open the app, look at it, and make sure it matched the requirements.&lt;/p&gt;

&lt;p&gt;I also ran auto mode. I bounced between models, and while Claude 4.8 is great, it does chew through a lot of credits. Auto mode did really well, especially paired with SDD. That's one of the underrated parts of this approach. When everything is already written down in the spec, you don't need the greatest state-of-the-art model to get what you need. Auto mode was more than enough for most of this.&lt;/p&gt;

&lt;p&gt;On top of that I used some skills. The &lt;a href="https://github.com/aws/agent-toolkit-for-aws" rel="noopener noreferrer"&gt;AWS Agent Toolkit&lt;/a&gt;, plus the Amplify and serverless skills. I ran &lt;code&gt;/aws-amplify&lt;/code&gt; and it helped me get the game hosted on Amplify hosting. For the music I used ElevenLabs. I had a scripts folder wired up to a bunch of services, PixelLab for art, ElevenLabs for audio, even Hugging Face. All of it driven from inside Kiro, with a &lt;code&gt;.env&lt;/code&gt; file holding the keys, and it generated the assets for me too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;That's the basic version of my spec-driven development workflow.  It's more planning up front, but for a real feature it pays off.&lt;/p&gt;

&lt;p&gt;If you want to try the game, check it &lt;a href="http://programwitherik.com/kirogame" rel="noopener noreferrer"&gt;out&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Do you use spec-driven development in your day-to-day? I'm curious what your experience has been. Take care, and thanks for reading.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>kiro</category>
      <category>programming</category>
    </item>
    <item>
      <title>TanStack Start vs Nuxt: One Framework to rule them all?</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 07 Jul 2026 21:38:55 +0000</pubDate>
      <link>https://dev.to/erikch/tanstack-vs-nuxt-one-framework-to-rule-them-all-4acl</link>
      <guid>https://dev.to/erikch/tanstack-vs-nuxt-one-framework-to-rule-them-all-4acl</guid>
      <description>&lt;p&gt;I love Nuxt and I really like TanStack Start. But which one is better? Or are they about the same?&lt;/p&gt;

&lt;p&gt;And if they are about the same, does it do anything my Nuxt setup can't, and is that worth leaving Vue for React? So I decided to build the same app in both frameworks and take a look. Read on below to find out!&lt;/p&gt;

&lt;p&gt;If you'd rather watch a video, check out the video on the same topic!&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/DBLXThKhTsY"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The app
&lt;/h2&gt;

&lt;p&gt;In both frameworks I built a small GitHub user lookup app. You type a username, the profile gets fetched on the server, and the username lands in the URL as a &lt;code&gt;?user=&lt;/code&gt; query param so the result is shareable. Type &lt;code&gt;ErikCH&lt;/code&gt;, hit enter, and the card renders. Refresh the page and it's still there. It has the same behaviour so the difference lies in the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Difference one: server functions vs server routes
&lt;/h2&gt;

&lt;p&gt;On the Nuxt side we call a server route from &lt;code&gt;useAsyncData&lt;/code&gt;. Server routes are the more idiomatic way to use Nuxt to call things on the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/pages/index.vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GithubUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~~/server/api/github.get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;definePageMeta&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defineProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;useAsyncData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;$fetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GithubUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/github&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;props&lt;/code&gt; option on &lt;code&gt;definePageMeta&lt;/code&gt; maps the query into a typed page prop and re-runs on client navigation. &lt;code&gt;useAsyncData&lt;/code&gt; fetches when there's a username and refetches whenever it changes. The conditional that returns &lt;code&gt;Promise.resolve(null)&lt;/code&gt; skips the request on an empty query param, (or when you first load).&lt;/p&gt;

&lt;p&gt;The server route does the outbound call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/api/github.get.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;querySchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Missing ?user=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GithubUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getValidatedQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;querySchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;$fetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GithubUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://api.github.com/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getValidatedQuery&lt;/code&gt; runs the schema and hands back a typed, validated &lt;code&gt;user&lt;/code&gt;. A bad shape, like an array or an empty string, gets a 400 before the handler runs.&lt;/p&gt;

&lt;p&gt;TanStack Start takes a different route. There's no separate API endpoint. You write a server function and call it directly from the loader.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/routes/index.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createServerFn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getGithubUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createServerFn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inputValidator&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GithubUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`https://api.github.com/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`GitHub API error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;GithubUser&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One API covers reads and writes. This same &lt;code&gt;createServerFn&lt;/code&gt; handles a GET here, and it would handle a POST mutation the same way, called from a loader or straight from a component. The return type flows through to the caller with no manual annotation.&lt;/p&gt;

&lt;p&gt;The route wires it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;validateSearch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;searchSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;loaderDeps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getGithubUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;validateSearch&lt;/code&gt; defines the query, &lt;code&gt;loaderDeps&lt;/code&gt; narrows it to just what the loader needs, and the loader runs on navigation, a lot like &lt;code&gt;script setup&lt;/code&gt; running when a Nuxt page loads.&lt;/p&gt;

&lt;p&gt;Between the two, I really like TanStack and it's server functions. I don't have to worry about another end point being exposed to the internet, and it's simple to use. Though both work for a lot of different scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server components in both
&lt;/h2&gt;

&lt;p&gt;Both frameworks also let you skip the client entirely with server components, and both still label the feature experimental.&lt;/p&gt;

&lt;p&gt;Nuxt calls them islands. You flip one flag in &lt;code&gt;nuxt.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;componentIslands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a component named with a &lt;code&gt;.server.vue&lt;/code&gt; suffix renders only on the server. It can call &lt;code&gt;useFetch&lt;/code&gt; directly, and no client-side JavaScript ships for it. Basically you embed the server component in a client component and that's it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/components/GithubUserCard.server.vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GithubUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~~/server/api/github.get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defineProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;useFetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GithubUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/github&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The card fetches on the server and streams its HTML into the page. When &lt;code&gt;?user=&lt;/code&gt; changes, Nuxt refetches the island in place. The GitHub response never lands in the browser bundle, which is nice to help reduce bundle size.&lt;/p&gt;

&lt;p&gt;TanStack Start added server components too, back in April. They're also experimental, and from the docs they look pretty different from the Next.js version people know, built around a readable-stream API. I tried it and found it more complicated than the Nuxt island, so for this app I stayed on the server function.&lt;/p&gt;

&lt;p&gt;Of everything here, the server function is still my favorite. Server routes earn their place when something outside your app needs to call in, like a webhook, or some sort of callback. For the rest, the function felt better and cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typed search params
&lt;/h2&gt;

&lt;p&gt;This is where TanStack Start made me a little jealous.&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;validateSearch&lt;/code&gt; puts the query schema on the route itself, the &lt;code&gt;user&lt;/code&gt; param is typed everywhere it shows up. Not only in the component, but in navigation and links.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Typed against the route's search schema:&lt;/span&gt;
&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Link&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ErikCH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Look&lt;/span&gt; &lt;span class="nx"&gt;up&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Link&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass a key that isn't in the schema and you get a red squiggle before you save. Delete the target on a &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; and the editor lists the real routes, because the routes are typed as well. I could see me using this all over, and if you're using an agent to help code, this will help catch the errors even quicker.&lt;/p&gt;

&lt;p&gt;Nuxt gets you close, with two caveats. Turn on &lt;code&gt;experimental.typedPages&lt;/code&gt; and your &lt;code&gt;&amp;lt;NuxtLink&amp;gt;&lt;/code&gt; routes are typed, so that half matches. With the &lt;code&gt;props&lt;/code&gt; plus Zod pattern above, the &lt;code&gt;user&lt;/code&gt; value reaching your component is typed and validated too. What Nuxt doesn't give you yet is the query itself typed on the link. You can type the prop, but not the &lt;code&gt;?user=&lt;/code&gt; on a &lt;code&gt;&amp;lt;NuxtLink&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you are on the Nuxt team, does anyone know a workaround? Leave a comment? I'd love a typed-query-params story on the router in Nuxt.&lt;/p&gt;

&lt;h2&gt;
  
  
  What they actually share
&lt;/h2&gt;

&lt;p&gt;Interesting enough, both of these frameworks share a lot. Both build on Vite, so both dev servers start are super fast. Both run on Nitro for the server layer, which adds a lot of flexibility when deploying to different places. Overall in that sense they are very similar.&lt;/p&gt;

&lt;p&gt;One practical thing to weigh. As of this recording TanStack Start is still a Release Candidate (RC), so expect a few rough edges and bugs. Nuxt is on 4.4.x, production-hardened, with a modules ecosystem for images, i18n, SEO, and a lot that TanStack Start doesn't have yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=DBLXThKhTsY" rel="noopener noreferrer"&gt;Watch the video&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nuxt.com" rel="noopener noreferrer"&gt;Nuxt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tanstack.com/start/latest/docs" rel="noopener noreferrer"&gt;TanStack Start docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tanstack.com/blog/announcing-tanstack-start-v1" rel="noopener noreferrer"&gt;TanStack Start v1 Release Candidate announcement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tanstack.com/start/latest/docs/framework/react/guide/server-components" rel="noopener noreferrer"&gt;TanStack Start server components guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zod.dev" rel="noopener noreferrer"&gt;Zod&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vite.dev" rel="noopener noreferrer"&gt;Vite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nitro.build" rel="noopener noreferrer"&gt;Nitro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The verdict
&lt;/h2&gt;

&lt;p&gt;So, should a Nuxt developer switch? For me, no, at least not today. I'm staying on Nuxt. I like Vue, the framework is mature, and TanStack features are nice, but not enough for me to move over (specially since it's still React).&lt;/p&gt;

&lt;p&gt;If you're already a React developer, or starting something new in React, TanStack Start is genuinely fun and I'd reach for it. React fans will feel at home in TanStack Start, Vue fans in Nuxt. I'll keep an eye on TanStack Start and pull it out for the odd project every now and then.&lt;/p&gt;

&lt;p&gt;One last note. I did all the research for the video and this post in &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;, an agent that works on the IDE, CLI and Web. &lt;/p&gt;

&lt;p&gt;If you've run both, tell me which way you'd go in the comments.&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>tanstack</category>
      <category>vue</category>
      <category>react</category>
    </item>
    <item>
      <title>Loop Engineering: Do Frontend and Fullstack Devs Actually Need It?</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 30 Jun 2026 17:59:18 +0000</pubDate>
      <link>https://dev.to/erikch/loop-engineering-do-frontend-and-fullstack-devs-actually-need-it-48eb</link>
      <guid>https://dev.to/erikch/loop-engineering-do-frontend-and-fullstack-devs-actually-need-it-48eb</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I keep hearing the term loop engineering. It's all over my feed, every AI newsletter, half the dev videos I open. So I had the same question you probably have. Is this actually important, and do I need to learn it as a frontend or fullstack developer?&lt;/p&gt;

&lt;p&gt;I spent some time with it and made a video breaking down what it is and where it fits. This post is the written version, with a practical example you can copy.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Q1tgkuCcQBo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What loop engineering actually is
&lt;/h2&gt;

&lt;p&gt;Here's how I think about it. You give your coding agent a goal, and it runs over itself again and again, almost recursively, until that goal is met.&lt;/p&gt;

&lt;p&gt;You can also frame it as four parts. You give the agent a trigger or a system prompt with a clear ending state. It acts on that trigger. It observes to check whether the work is actually done. If it hits the stop condition, it stops. If it doesn't, it goes around again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft007ezbk7lnn3ddi1srv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft007ezbk7lnn3ddi1srv.png" alt="Anatomy of a loop" width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's basically it. The idea is that you are defining a goal and  a stop condition, then letting the agent close the gap on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's the agentic loop you already know
&lt;/h2&gt;

&lt;p&gt;If you've built anything with agents, this should feel familiar. It's the classic agentic loop. You give an agent a set of tools, and it uses those tools with its own reasoning to reach an answer.&lt;/p&gt;

&lt;p&gt;Say I want to research the latest tech trends. I give the agent a tool to search Google and a tool to search Reddit. On the first pass it searches Google and pulls what it finds. That isn't enough, so it loops again and pulls from Reddit. Then it combines both and hands the answer back to me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjt9tocg85rfm4hubn6eu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjt9tocg85rfm4hubn6eu.png" alt="Agentic Loop" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You've seen the same thing in a chatbot. Ask it for the weather and it might call a few tools, look at the context of your question, and come back with what you asked for. Loop engineering is that pattern, pointed at your actual work.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical example: fixing a PR on a loop
&lt;/h2&gt;

&lt;p&gt;Let me show you where this earns its place day to day.&lt;/p&gt;

&lt;p&gt;I had a pull request that I pushed up using &lt;a href="https://kiro.dev/web" rel="noopener noreferrer"&gt;Kiro Web&lt;/a&gt;. It connected straight to my GitHub repo, I gave it an action, and it opened the PR for me. You can do this with Claude Code, Cursor, and other agents too.&lt;/p&gt;

&lt;p&gt;Then I noticed some tests were failing in my CI/CD pipeline. My GitHub Actions were red. So I gave it one prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fix all issues on the PR. Keep going until it's all fixed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last sentence is the stop condition. Kiro went to work in the background, running again and again, fixing the failures and looping until everything passed. I didn't babysit it through each round.&lt;/p&gt;

&lt;p&gt;Tests are a great place to start with this, because the pass or fail signal gives the loop a clear way to check its own work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automations are the other big use case
&lt;/h2&gt;

&lt;p&gt;The other place loops shine is on a schedule. &lt;a href="https://addyosmani.com/blog/loop-engineering/" rel="noopener noreferrer"&gt;Addy Osmani&lt;/a&gt; wrote a great article on loop engineering where he points out that automations running on a schedule, doing discovery and triage on their own, are a strong fit for this.&lt;/p&gt;

&lt;p&gt;If you can find work that a cron job or a scheduled tool can kick off every day, that's a candidate. A couple of examples:&lt;/p&gt;

&lt;p&gt;At the end of every day, run all the tests, and if anything is broken, let the agent fix it. Or keep your documentation current. The loop checks the docs against the latest code and keeps going until they match.&lt;/p&gt;

&lt;p&gt;If you use Claude Code, there's a &lt;code&gt;/loop&lt;/code&gt; command and a &lt;code&gt;/goal&lt;/code&gt; command worth knowing. They let you set up in-session scheduled work, and &lt;code&gt;/goal&lt;/code&gt; lets you set a persistent, verifiable objective instead of a single one-off task. You can do the same thing in Kiro and other tools, you just have to be more explicit and write your own system prompts to drive it.&lt;/p&gt;

&lt;h2&gt;
  
  
  My take
&lt;/h2&gt;

&lt;p&gt;Now the part where I push back a little.&lt;/p&gt;

&lt;p&gt;Don't tear up your whole workflow to jam loops in everywhere. There are real spots where they make sense, especially when your agent supports them and you've found an automation worth setting up. But I still think spec-driven development and plain back-and-forth vibe coding will solve most of your problems day to day. Loops are a tool to reach for in the right moment, not a religion.&lt;/p&gt;

&lt;p&gt;If you want to go deeper, three people shaped how I think about this. Kent C. Dodds made an excellent video on what he's discovered and how he works this way. Theo did a great one too. And Addy Osmani's article on loop engineering is worth your time.&lt;/p&gt;

&lt;p&gt;So I'll ask you what I asked at the end of the video. On your next project, are you going to try loop engineering? Or is it just hype? Let me know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=jCZKiHrUNT4" rel="noopener noreferrer"&gt;Kent C. Dodds on loop engineering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=iJVJwmCKW9o" rel="noopener noreferrer"&gt;Theo (t3.gg) on loops&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://addyosmani.com/blog/loop-engineering/" rel="noopener noreferrer"&gt;Addy Osmani — Loop Engineering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev/web/" rel="noopener noreferrer"&gt;Kiro Web&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>3 Tools That Make AI Suck Less at Coding</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Mon, 22 Jun 2026 00:47:45 +0000</pubDate>
      <link>https://dev.to/erikch/3-tools-that-make-ai-suck-less-at-coding-4c3</link>
      <guid>https://dev.to/erikch/3-tools-that-make-ai-suck-less-at-coding-4c3</guid>
      <description>&lt;p&gt;I've completely updated my workflow, and I use AI coding agents for everything. Writing code, testing, and just helping me be more productive. However, AI isn't perfect and often after a long back-and-forth session I'll have working code, but it will have a few code smells. Duplicated code, dead-end code, lengthy functions are common. One way to fix that is to add all these problems to an AGENTS.md file and that works. But that's not always possible, depending on the codebase and team I'm working with.&lt;/p&gt;

&lt;p&gt;To that end, I went looking for tools that could help me detect and fix these issues. And so after some research I found three tools that could work almost as a pipeline. One finds these issues, one reviews the changes before they ever reach a pull request, and one helps me evaluate my agents. I use &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; for most of my day to day, so I wired all of these into that loop, but they run with whatever agent you already have.&lt;/p&gt;

&lt;p&gt;If you'd rather watch than read, here's the full walkthrough:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/NMJ_CfB11x0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;This post covers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finding dead code and duplication with Fallow&lt;/li&gt;
&lt;li&gt;Reviewing the AI's diff with CodeRabbit before you open a PR&lt;/li&gt;
&lt;li&gt;Measuring whether the output improves with Kiln&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 or newer, so you have &lt;code&gt;npx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A JavaScript or TypeScript project, ideally one with some AI-generated code in it&lt;/li&gt;
&lt;li&gt;A coding agent like &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;, Claude Code, or Cursor for the review-loop parts&lt;/li&gt;
&lt;li&gt;Git, for the diff-based workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Find the mess: Fallow
&lt;/h2&gt;

&lt;p&gt;The first problem I've seen often is that AI can generate a mess of code. It will duplicate a block, leave an orphaned file behind, and grow a function to a thousand lines without ever flagging it, because each individual edit looked reasonable in isolation. You need a tool that reads the whole codebase at once.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/fallow-rs/fallow" rel="noopener noreferrer"&gt;Fallow&lt;/a&gt; is the one I reached for. It's static codebase intelligence for JS and TS, and it works a lot like ESLint, except it's built around these common AI code issues. You can run it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx fallow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a full report. The sections I care about most are dead code, duplication, and complexity. Dead code finds files that aren't reachable from any entry point, exports nothing imports, and dependencies you've stopped using. &lt;strong&gt;When I ran this on my vibe-coded chess app (see video) it found dozens of these problems&lt;/strong&gt;  Duplication shows you the exact line ranges that got copied between two places. Complexity scores each function on how branchy and how hard to test it is, then ranks your worst offenders so you know where to start.&lt;/p&gt;

&lt;p&gt;There's a fix command for most things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx fallow fix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather have an agent fix it you can use a JSON output and the skill. Run it with &lt;code&gt;--json&lt;/code&gt; and you get structured findings an agent can read and act on. Fallow also ships an agent skill you can install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add fallow-rs/fallow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that's in place, I can tell &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; to build a feature and then run Fallow on its own work to clean up after itself. It catches the duplicate block it just wrote and removes it before I even see it. You can also drop &lt;code&gt;fallow audit&lt;/code&gt; into CI so it compares your branch against main and only flags what your change introduced.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonus: Knip
&lt;/h3&gt;

&lt;p&gt;If Fallow is more than you need, &lt;a href="https://knip.dev" rel="noopener noreferrer"&gt;Knip&lt;/a&gt; does the dead-code half on its own and has been around longer. It finds unused files, dependencies, and exports in JS and TS projects in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx knip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've found knip to be a little slower then Fallow. YMMV.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the diff: CodeRabbit
&lt;/h2&gt;

&lt;p&gt;Finding dead code is great, but what about reviews?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.coderabbit.ai/" rel="noopener noreferrer"&gt;CodeRabbit&lt;/a&gt; puts a review step back in. It's AI code review that runs on your diff, and the version I like runs locally from the CLI so it happens before the PR exists. Install it with one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://cli.coderabbit.ai/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point it at your uncommitted changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;coderabbit review &lt;span class="nt"&gt;--plain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads the diff and comes back with specific findings. It also hands you a fix prompt you can paste straight back into your agent, so the loop closes quickly.&lt;/p&gt;

&lt;p&gt;Jack Herrington made the point in his video that lower-end models get noticeably more useful once CodeRabbit is in the loop, because the review catches what the cheaper model missed. That makes sense to me. The agent writes, CodeRabbit reviews, the agent fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the improvement: Kiln
&lt;/h2&gt;

&lt;p&gt;The first two tools clean up individual changes. This last one answers a bigger question. When I tweak a prompt, swap a model, or add a skill, is my agent's output actually better, or does it just feel better because the last three runs went well?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Kiln-AI/Kiln" rel="noopener noreferrer"&gt;Kiln&lt;/a&gt; is the tool I use to stop guessing. It's built for creating and evaluating AI systems, so you set up datasets, run evals, and compare results across changes. &lt;/p&gt;

&lt;p&gt;For my use case, I connected it to some output a LLM was producing. I ran an eval on it to make sure it was as I expected. When I added the Fallow skill, I could iterate on the system prompt and strings. It's the same instinct behind writing tests for your code, pointed at the thing generating the code.&lt;/p&gt;

&lt;p&gt;This is the least flashy of the three and the one I'd skip first if you're in a hurry. But if you're serious about tuning a workflow you'll use every day, measuring beats vibes every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to pick
&lt;/h2&gt;

&lt;p&gt;You don't have to use all three, so here are some good guide lines for each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your agent leaves dead code and duplication everywhere, start with Fallow, or Knip if you want something smaller.&lt;/li&gt;
&lt;li&gt;If you need more code reviews, add CodeRabbit and let it review before you do.&lt;/li&gt;
&lt;li&gt;If you're tuning prompts, models, or skills and want proof you're improving, set up Kiln.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep in mind these tools don't replace knowing what good code looks like. They raise the floor on what your AI ships so the version that lands in your editor is closer to something you'd have written yourself. That's extremely important. AI made writing code fast, and it made reviewing code the bottleneck. These three move that bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AI compresses how long it takes to generate code and stretches how long it takes to trust it. Fallow and Knip find the mess it leaves behind, CodeRabbit reviews the change before it slips past you, and Kiln tells you whether any of your tuning is working. &lt;/p&gt;

&lt;p&gt;If you want to see these running inside a real agent loop, I walk through all of them in the &lt;a href="https://www.youtube.com/watch?v=NMJ_CfB11x0" rel="noopener noreferrer"&gt;companion video&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/fallow-rs/fallow" rel="noopener noreferrer"&gt;Fallow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://knip.dev" rel="noopener noreferrer"&gt;Knip&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.coderabbit.ai/" rel="noopener noreferrer"&gt;CodeRabbit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Kiln-AI/Kiln" rel="noopener noreferrer"&gt;Kiln&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tooling</category>
    </item>
    <item>
      <title>The most popular AI coding skills right now</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Mon, 15 Jun 2026 17:04:07 +0000</pubDate>
      <link>https://dev.to/aws/the-most-popular-ai-coding-skills-right-now-4183</link>
      <guid>https://dev.to/aws/the-most-popular-ai-coding-skills-right-now-4183</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;It's crazy to me that some GitHub repos, that were just created in the last year, have more stars then some of the most popular programming frameworks out there. At the time of writing, &lt;a href="https://github.com/obra/superpowers" rel="noopener noreferrer"&gt;Superpowers&lt;/a&gt;, is around 226,000, ahead of the Vue repo and way ahead of Next.js. A separate repo based on Andrej Karpathy's coding advice has 174,000. And they aren't even code. They're folders of markdown files that tell your AI coding agent how to write software better.&lt;/p&gt;

&lt;p&gt;In other words these are agent skills, and they've blown up over the past year into one of the most-starred new categories of developer tooling. The format started at Anthropic, a &lt;code&gt;SKILL.md&lt;/code&gt; file with a short description and a set of instructions, and now it works in Claude Code, &lt;a href="https://kiro.dev/" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; and all your other favorite coding agents.&lt;/p&gt;

&lt;p&gt;This post is a tour of some of the skills I find most interesting that people are using everyday. I'll be in Kiro for most of this, but these run in whatever agent you already have, with install paths that vary per agent.&lt;/p&gt;

&lt;p&gt;This post covers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What an agent skill actually is&lt;/li&gt;
&lt;li&gt;The big workflow skills (Superpowers, Karpathy, Matt Pocock, GStack, GSD)&lt;/li&gt;
&lt;li&gt;Framework skills and the "ships with your package" model&lt;/li&gt;
&lt;li&gt;The Agent Toolkit for AWS&lt;/li&gt;
&lt;li&gt;How to pick one and try it safely&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you like, you can watch a &lt;a href="https://youtu.be/enWhBCyU36U?si=wySkthIQYr6592m5" rel="noopener noreferrer"&gt;full video&lt;/a&gt; on the subject instead! Also if nothing else, please check out the &lt;a href="https://github.com/aws/agent-toolkit-for-aws" rel="noopener noreferrer"&gt;Agent Toolkit for AWS&lt;/a&gt;! More information below. &lt;em&gt;Full disclosure, I'm a developer advocate for AWS&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an agent skill?
&lt;/h2&gt;

&lt;p&gt;A skill is a directory with a &lt;code&gt;SKILL.md&lt;/code&gt; file inside it. The file has a name, a one-line description, and instructions. These instructions include which steps to follow, which APIs to call, which mistakes to avoid and how to check the result.&lt;/p&gt;

&lt;p&gt;Skills should only run when needed. Your agent reads the short description, decides if the skill is relevant to what you asked, and only then loads the full instructions. Reference files load on demand after that. So you can have fifty skills installed and your context window stays clean, because the agent pulls in the two it needs and ignores the rest. &lt;em&gt;In practice, I've found I still need to manually tell my agent to run the skills by using the &lt;code&gt;/&lt;/code&gt; command and the skill name. YMMV&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The big workflow skills
&lt;/h2&gt;

&lt;p&gt;Most of the popular general-purpose skills are trying to fix the problem agents often have, going in the wrong direction. Agents when left alone often rush in, touch too many files, and they skip planning. These five take different approaches to slowing it down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Superpowers (obra/superpowers)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/obra/superpowers" rel="noopener noreferrer"&gt;Superpowers&lt;/a&gt; is the most-starred of the bunch and the most opinionated. It is a full development methodology that triggers automatically. It really reminds me of a super powered Spec-Driven Development (SDD) workflow. &lt;/p&gt;

&lt;p&gt;The moment the agent sees you are building something, it stops and asks what you are actually trying to do. It works a spec out of the conversation, shows it to you (optionally in a web browser), and only after you sign off does it write an implementation plan. Then it hands the plan to subagents that build and review each other's work. Be aware, this can eat up a lot of tokens.&lt;/p&gt;

&lt;p&gt;It is available through the official Claude plugin marketplace, plus Codex, Cursor, Gemini CLI, and a handful of others. You can also get it working with Kiro by from &lt;a href="https://www.skills.sh/obra/superpowers/brainstorming" rel="noopener noreferrer"&gt;Skills.sh&lt;/a&gt;. Though, I'd probably skip it if you're using Kiro, since most of the SDD is already there in Kiro. &lt;/p&gt;

&lt;p&gt;If you want one system that owns the whole loop from idea to reviewed code, this is the one to reach for. In Claude Code, install it from the official plugin marketplace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin install superpowers@claude-plugins-official
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Karpathy guidelines (multica-ai/andrej-karpathy-skills)
&lt;/h3&gt;

&lt;p&gt;This one is almost the opposite. It is a short file of behavioral rules, now at &lt;a href="https://github.com/multica-ai/andrej-karpathy-skills" rel="noopener noreferrer"&gt;174,000 stars&lt;/a&gt;. Andrej Karpathy did not write it, despite the name, which I find really funny. It is a community-built file derived from a January post where Karpathy listed the ways LLM coding agents tend to fail, and the repo later moved to the multica-ai org.&lt;/p&gt;

&lt;p&gt;The rules come down to four simple ideas. Think before coding, keep it simple, make surgical changes, and stay focused on the goal. If Superpowers feels like too much process for you, this is the low-commitment starting point. It is a single &lt;code&gt;CLAUDE.md&lt;/code&gt; (or AGENTS.md, or you can use it as a Skill), so the simplest install is to drop it into your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; CLAUDE.md https://raw.githubusercontent.com/multica-ai/andrej-karpathy-skills/main/CLAUDE.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Personally, I'm not sure if this is worth it in my own testing. However, it's easy enough to add it to your own &lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;AGENTS.md&lt;/code&gt; so give it a shot though and make your own opinion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Matt Pocock's skills (mattpocock/skills)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/mattpocock/skills" rel="noopener noreferrer"&gt;Matt Pocock's collection&lt;/a&gt; sits in the middle and takes a deliberate stance against the all-in-one systems. In fact, he's often said he doesn't like SDD, and thinks it's too cumbersome. His framing is that approaches which own your entire process also take away your control and make bugs in that process hard to fix. So his skills are small, composable, and easy to adapt. You install the ones you want with the Vercel &lt;code&gt;skills&lt;/code&gt; CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills@latest add mattpocock/skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;His most popular one, and the one I like the most, is the &lt;code&gt;grill-me&lt;/code&gt;, which makes the agent interview you about what you are building before it writes anything. There is a more powerful version, &lt;code&gt;grill-with-docs&lt;/code&gt;, that does the same interview and also builds a shared-language document for your project. The repo's pitch is that this shared-language document pays off across sessions. There is also &lt;code&gt;tdd&lt;/code&gt; for a red-green-refactor loop, &lt;code&gt;diagnose&lt;/code&gt; for debugging, and &lt;code&gt;improve-codebase-architecture&lt;/code&gt; for rescuing a project that has turned into spaghetti.&lt;/p&gt;

&lt;p&gt;Another skill called &lt;code&gt;caveman&lt;/code&gt; helps reduce token costs. I find the &lt;code&gt;caveman&lt;/code&gt; and &lt;code&gt;grill-me&lt;/code&gt; skills the most useful. Though, I find &lt;code&gt;grill-me&lt;/code&gt; a little annoying after a while, when I just want to get something done. &lt;/p&gt;

&lt;h3&gt;
  
  
  GStack (garrytan/gstack)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/garrytan/gstack" rel="noopener noreferrer"&gt;GStack&lt;/a&gt; is Garry Tan's Claude Code setup, and at nearly 110,000 stars it is one of the most popular skill collections out there. Instead of one mode that handles everything, it gives the agent distinct roles, each behind its own slash command. It has product vision, designer, engineering manager, release manager, doc engineer, QA, and post-launch retrospective. It is the same genre as Superpowers, a full workflow, but organized around the roles on a software team rather than a sequence of phases. If you think in terms of "who would handle this part," GStack maps to that mental model nicely. It installs with a clone and a setup script (pass &lt;code&gt;--host kiro&lt;/code&gt;, &lt;code&gt;--host cursor&lt;/code&gt;, and so on for other agents):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--single-branch&lt;/span&gt; &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/.claude/skills/gstack &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also works with Kiro nicely.&lt;/p&gt;

&lt;h3&gt;
  
  
  GSD Core (open-gsd/gsd-core)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/open-gsd/gsd-core" rel="noopener noreferrer"&gt;GSD Core&lt;/a&gt; (Git. Ship. Done.) is a spec-driven system that pitches itself as a fix for context rot, the slow drop in quality as your context window fills up during a long session. It breaks work into atomic plans that run in fresh context windows, keeping the main session light while subagents do the heavy lifting. It targets more than a dozen agents. The project moved from the old &lt;code&gt;gsd-build&lt;/code&gt; org to its current home at &lt;code&gt;open-gsd/gsd-core&lt;/code&gt;, and the installer prompts for your runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @opengsd/gsd-core@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While using this, I found that it burned through tokens fast. It also takes over your whole workflow, that I disliked. Still, it's worth trying out to see if it works well for your own personal projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framework skills and the "ships with your package" model
&lt;/h2&gt;

&lt;p&gt;The skills above are framework-agnostic. The more interesting development for frontend folks is skills that know your specific stack.&lt;/p&gt;

&lt;p&gt;The Vue community moved early here. &lt;a href="https://github.com/vuejs-ai/skills" rel="noopener noreferrer"&gt;vuejs-ai/skills&lt;/a&gt; is a community Vue 3 skills project (the README notes the author may propose transferring it to the Vue org), and its &lt;code&gt;vue-best-practices&lt;/code&gt; skill is a structured workflow covering the Composition API, reactivity, and the patterns you want a model to follow. &lt;a href="https://github.com/antfu/skills" rel="noopener noreferrer"&gt;Anthony Fu's collection&lt;/a&gt; covers Vue, Vite, Nuxt, Pinia, VueUse, and Vitest. Both install through the same CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add vuejs-ai/skills
npx skills add antfu/skills &lt;span class="nt"&gt;--skill&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The newer idea is shipping skills inside the package itself. Three things are worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/skilld-dev/skilld" rel="noopener noreferrer"&gt;skilld&lt;/a&gt; generates version-aware skills from the npm dependencies you already have, pulling from docs, release notes, and GitHub issues. It solves the problem where a model's training cutoff means it writes last year's version of your library.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tanstack.com/intent" rel="noopener noreferrer"&gt;TanStack Intent&lt;/a&gt; (&lt;code&gt;@tanstack/intent&lt;/code&gt;) flips that around for maintainers. It lets library authors generate and ship &lt;code&gt;SKILL.md&lt;/code&gt; files inside their npm package, versioned with each release, so the skill updates when the library does. It launched recently and ties straight into the TanStack ecosystem.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/vercel-labs/skills" rel="noopener noreferrer"&gt;Vercel skills CLI&lt;/a&gt; is the &lt;code&gt;npx skills&lt;/code&gt; tool that installs any of this across 70-plus agents. If you use Kiro, it loads skills from &lt;code&gt;.kiro/skills/&lt;/code&gt; automatically with no config, which makes it an easy way to try things on camera or in a real project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is each of those three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate version-aware skills from your installed npm dependencies&lt;/span&gt;
npx &lt;span class="nt"&gt;-y&lt;/span&gt; skilld

&lt;span class="c"&gt;# Library maintainers: scaffold SKILL.md files inside your own package&lt;/span&gt;
npx @tanstack/intent scaffold

&lt;span class="c"&gt;# The universal installer used throughout this post — pattern is `npx skills add &amp;lt;owner/repo&amp;gt;`&lt;/span&gt;
npx skills add vercel-labs/skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Agent Toolkit for AWS
&lt;/h2&gt;

&lt;p&gt;AWS ships skills too, in the same open format, so they run in Kiro, Claude Code, Cursor, and Codex.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.aws.amazon.com/agent-toolkit/latest/userguide/skills.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Agent Toolkit for AWS&lt;/a&gt; was announced in May 2026 and is the officially supported way to bring AWS expertise into your agent. It pairs plugins for Claude Code and Codex with a curated collection of skills and the &lt;a href="https://aws.amazon.com/blogs/aws/the-aws-mcp-server-is-now-generally-available/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS MCP Server&lt;/a&gt;, which reached general availability on May 6, 2026. The toolkit covers areas like deployment, serverless, and database work. Setup is per-agent. In Kiro you add the AWS MCP Server through your MCP config and then pull in skills with &lt;code&gt;npx skills&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install AWS-curated skills via the universal CLI&lt;/span&gt;
npx skills add aws/agent-toolkit-for-aws/skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are building on AWS,  you should download this. Even if you are not using AWS I would download it for the future. Because with this the agent loads service-specific guidance instead of guessing at an API it half-remembers from training.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to pick one and try it safely
&lt;/h2&gt;

&lt;p&gt;Personally, I would use Matt Pococks skills and try out one of the other larger skill workflows like Superpowers. See if it works for you. Then pair it with a framework specific skills as well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Last thing, and it matters. A skill is instructions you are dropping into your agent's context, so treat it the way you treat an npm dependency. Read the &lt;code&gt;SKILL.md&lt;/code&gt; before you install it. A repo with 100,000 stars is popular, but stars are a popularity signal, not a quality review. Skim the file, see what it tells your agent to do, and keep the ones that match how you actually work.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Agent skills went from a quiet Anthropic format to repos with more stars than the frameworks we build on, and it makes sense why. They take the patterns a good engineer carries in their head and hand them to the model at the start of every task. The big workflow skills give you structure while the framework skills give you correctness, and the AWS plugins give you service-specific knowledge you would otherwise paste in by hand.&lt;/p&gt;

&lt;p&gt;My advice: install one, read what it actually tells the agent, and see if the output matches how you'd have built it yourself. That's the real test, and it's the one only you can run. If you want to see these in action, I walk through them in the companion video.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>aws</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
