<?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: Yoshinori Ishii</title>
    <description>The latest articles on DEV Community by Yoshinori Ishii (@yoshinoriishii).</description>
    <link>https://dev.to/yoshinoriishii</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%2F1309505%2Fe145ab6f-d575-4623-947b-0710090f55f1.jpg</url>
      <title>DEV Community: Yoshinori Ishii</title>
      <link>https://dev.to/yoshinoriishii</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yoshinoriishii"/>
    <language>en</language>
    <item>
      <title>Rethinking Vanilla CSS for the AI Era</title>
      <dc:creator>Yoshinori Ishii</dc:creator>
      <pubDate>Tue, 08 Sep 2026 00:54:39 +0000</pubDate>
      <link>https://dev.to/yoshinoriishii/rethinking-vanilla-css-for-the-ai-era-lbm</link>
      <guid>https://dev.to/yoshinoriishii/rethinking-vanilla-css-for-the-ai-era-lbm</guid>
      <description>&lt;p&gt;I love vanilla CSS and want to keep using it as the language evolves. These days, AI agents write most of it for me. As my work shifted toward reviewing their output, I felt it was time to rethink CSS conventions.&lt;/p&gt;

&lt;p&gt;I want fewer naming decisions to make and review. Wherever a rule can settle a class name, the same HTML and component structure should lead to the same answer, whoever writes it. The choices that remain should be explicit, and a linter should check the result. I am happy with detailed rules if they mean less guesswork for both the agent and me.&lt;/p&gt;

&lt;p&gt;HTML already carries meaning through its native elements and ARIA roles. Choosing appropriate elements and &lt;a href="https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/" rel="noopener noreferrer"&gt;using ARIA where needed&lt;/a&gt; helps make the interface accessible and gives an agent reading the HTML clear information about each element's role and state. I wanted CSS naming to draw on that existing meaning, so there would be fewer names to invent and review.&lt;/p&gt;

&lt;p&gt;The markup already has a structure, too. Frameworks can keep markup and scoped styles together in a component, and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Nesting/Using" rel="noopener noreferrer"&gt;native CSS nesting&lt;/a&gt; lets nested CSS blocks visually reflect its parent-child relationships without a preprocessor. That suggested a way to make both naming and structure easier to review while continuing to use plain CSS.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://nagi-labs.github.io/nagi-css/" rel="noopener noreferrer"&gt;Nagi CSS&lt;/a&gt; to put that idea into practice: ordinary component CSS, with naming and structural rules that ESLint can check. The plugin supports Vue, Nuxt, Svelte, and Astro.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the HTML
&lt;/h2&gt;

&lt;p&gt;Consider the formatting controls you might put in a text editor: a small component that toggles Bold and Italic and reports which formats are active. The source filename, excluding its framework-specific extension, is &lt;code&gt;format-controls&lt;/code&gt;; the project uses the class prefix &lt;code&gt;app-&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, look just at the HTML the component renders with Bold active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"app-format-controls"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Text formatting&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"unit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Apply to the current selection&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"group"&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"group"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Text formatting"&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;class=&lt;/span&gt;&lt;span class="s"&gt;"button -bold"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;aria-pressed=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Bold
      &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;class=&lt;/span&gt;&lt;span class="s"&gt;"button -italic"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;aria-pressed=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Italic
      &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;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt; &lt;span class="na"&gt;aria-atomic=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Active formatting: Bold.
    &lt;span class="nt"&gt;&amp;lt;/span&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;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change the Bold button's &lt;code&gt;button&lt;/code&gt; class to &lt;code&gt;control&lt;/code&gt;, and ESLint reports that Nagi requires the class &lt;code&gt;button&lt;/code&gt; for a &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; element. The tag supplies the name, and the linter checks it. Here is how the names in the rest of the example are determined or chosen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the names come from
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;base class&lt;/strong&gt; identifies the element or part. In &lt;code&gt;class="button -bold"&lt;/code&gt;, it is &lt;code&gt;button&lt;/code&gt;; &lt;code&gt;-bold&lt;/code&gt; is a &lt;strong&gt;variant&lt;/strong&gt; that distinguishes the Bold control.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Naming rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;app-format-controls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The component source filename, without its framework-specific extension, plus the configured &lt;code&gt;app-&lt;/code&gt; prefix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The fixed mapping for heading elements, &lt;code&gt;h1&lt;/code&gt; through &lt;code&gt;h6&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;button&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The native element's tag name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;group&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The identifying &lt;code&gt;role&lt;/code&gt; on each &lt;code&gt;div&lt;/code&gt; or &lt;code&gt;span&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A name chosen from the allowed vocabulary for common UI parts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;div&lt;/code&gt; used only for layout, with no suitable role or UI part name; assigned from a predefined vocabulary explained below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;-bold&lt;/code&gt;, &lt;code&gt;-italic&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Static variants chosen by the author to distinguish the two button roles&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The base classes in the first four rows follow directly from the file, elements, roles, and project configuration. These mappings give CSS a consistent class vocabulary: a heading keeps &lt;code&gt;title&lt;/code&gt; whether its tag is &lt;code&gt;h2&lt;/code&gt; or &lt;code&gt;h3&lt;/code&gt;, while a role-based status message uses &lt;code&gt;status&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For a &lt;code&gt;div&lt;/code&gt; or &lt;code&gt;span&lt;/code&gt; without an identifying role, two fallbacks remain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UI anatomy&lt;/strong&gt; supplies a small vocabulary for common parts that HTML has no specific element for. Here, the short instruction uses &lt;code&gt;text&lt;/code&gt;. The default vocabulary is &lt;code&gt;field&lt;/code&gt;, &lt;code&gt;value&lt;/code&gt;, &lt;code&gt;actions&lt;/code&gt;, &lt;code&gt;media&lt;/code&gt;, &lt;code&gt;icon&lt;/code&gt;, and &lt;code&gt;text&lt;/code&gt;, and projects can configure it. Choosing the appropriate part still requires judgment, but does not require inventing a word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structural Tier Names (STN)&lt;/strong&gt; cover &lt;code&gt;div&lt;/code&gt;s that only arrange other elements and have no suitable role or UI part name. Instead of inventing names such as &lt;code&gt;wrapper&lt;/code&gt; or &lt;code&gt;inner&lt;/code&gt;, nested structural wrappers move from larger to finer parts through a fixed sequence: &lt;code&gt;unit&lt;/code&gt; → &lt;code&gt;seg&lt;/code&gt; (segment) → &lt;code&gt;fr&lt;/code&gt; (fragment) → &lt;code&gt;g&lt;/code&gt; (grain). This example has only one such &lt;code&gt;div&lt;/code&gt;, so it uses &lt;code&gt;unit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Variants let the author name stable distinctions such as Bold versus Italic. Anatomy involves choosing from an existing vocabulary; variants allow new words. Here, &lt;code&gt;-bold&lt;/code&gt; and &lt;code&gt;-italic&lt;/code&gt; identify each button's function, while its on/off state lives in &lt;code&gt;aria-pressed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The order matters: choose the HTML for its meaning, use its element or identifying role where available, then fall back to anatomy and structural tiers. The existing &lt;code&gt;role="status"&lt;/code&gt; determines &lt;code&gt;status&lt;/code&gt;; it was not added just to obtain that class name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write CSS against that structure
&lt;/h2&gt;

&lt;p&gt;Now use those names in the same component's stylesheet. Inside the component root, &lt;code&gt;&amp;gt;&lt;/code&gt; follows the actual parent-child relationships: the buttons are inside &lt;code&gt;.group&lt;/code&gt;, which is inside &lt;code&gt;.unit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You only need selector paths for the elements you style; unrelated DOM branches need no CSS blocks. The relevant styles are ordinary nested CSS:&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="nc"&gt;.app-format-controls&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;.title&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&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="nc"&gt;.unit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--space-4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;.text&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-text-muted&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="nc"&gt;.group&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--space-3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;.button&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;&amp;amp;.-bold&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;700&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;amp;&lt;/span&gt;&lt;span class="nc"&gt;.-italic&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;italic&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;amp;[&lt;/span&gt;&lt;span class="nt"&gt;aria-pressed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"true"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-accent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-accent-text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="err"&gt;}&lt;/span&gt;
    &lt;span class="err"&gt;}&lt;/span&gt;

    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;.status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-text-muted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that looks like a lot of CSS to type, that is the part I usually delegate to an AI agent. I still have to read and maintain the result. Following the same naming and nesting rules gives me a consistent structure to review, while the linter checks the names and paths against the markup.&lt;/p&gt;

&lt;p&gt;The buttons keep the same &lt;code&gt;button -bold&lt;/code&gt; and &lt;code&gt;button -italic&lt;/code&gt; classes as their state changes. The component updates &lt;code&gt;aria-pressed&lt;/code&gt;, and CSS reads that attribute directly. The same state is exposed to assistive technology and used to style the active format.&lt;/p&gt;

&lt;p&gt;The custom properties supply the project's design values. Nagi checks the contract; you choose the colors, spacing, and appearance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the linter check the connection
&lt;/h2&gt;

&lt;p&gt;Once names and selector paths follow rules, the linter can compare them with the source.&lt;/p&gt;

&lt;p&gt;For example, rename the component root to &lt;code&gt;.app-toolbar&lt;/code&gt; in both the markup and stylesheet. The styles can still work in the browser, but the lint rule named &lt;code&gt;nagi-css/surface-root-name&lt;/code&gt; reports that a component source named &lt;code&gt;format-controls&lt;/code&gt; requires &lt;code&gt;.app-format-controls&lt;/code&gt;. The expected name no longer depends on what an author happens to choose.&lt;/p&gt;

&lt;p&gt;It also checks relationships between otherwise valid names. This selector skips the group around the buttons:&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="nc"&gt;.app-format-controls&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;.unit&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;.button&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-accent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;unit&lt;/code&gt; and &lt;code&gt;button&lt;/code&gt; exist in the template, but &lt;code&gt;.unit &amp;gt; .button&lt;/code&gt; does not. Another lint rule, &lt;code&gt;nagi-css/selector-mirrors-template&lt;/code&gt;, reports the mismatch. The correct path is &lt;code&gt;.unit &amp;gt; .group &amp;gt; .button&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This matters during edits: when markup moves, a selector that still describes the old structure becomes a lint error. An agent and a human reviewer can check that relationship with the same command.&lt;/p&gt;

&lt;p&gt;The repository includes &lt;a href="https://github.com/nagi-labs/nagi-css/tree/main/tests/fixtures/profile-card" rel="noopener noreferrer"&gt;passing and failing fixtures&lt;/a&gt;, exercised by &lt;a href="https://github.com/nagi-labs/nagi-css/blob/main/tests/plugins.test.mjs" rel="noopener noreferrer"&gt;ESLint tests&lt;/a&gt;, so these checks can be run against actual component source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep each component's internals inside its boundary
&lt;/h2&gt;

&lt;p&gt;That structure check stops at another component's root. To see why, place the &lt;code&gt;format-controls&lt;/code&gt; component inside an &lt;code&gt;editor-panel&lt;/code&gt; component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"app-editor-panel"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;format-controls&amp;gt;&amp;lt;/format-controls&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component tag is framework-neutral shorthand here. At that position, the child renders the &lt;code&gt;.app-format-controls&lt;/code&gt; root shown earlier.&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="nc"&gt;.app-editor-panel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;.app-format-controls&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="py"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;44rem&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parent controls the child's placement and available width. The child controls its internal layout and appearance. Because the child already has its file-derived &lt;code&gt;app-format-controls&lt;/code&gt; root, the parent can select that root without passing another base class.&lt;/p&gt;

&lt;p&gt;Reaching inside the child breaks that division:&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="nc"&gt;.app-editor-panel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;.app-format-controls&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;　&lt;/span&gt;  &lt;span class="c"&gt;/* Invalid: .unit belongs to format-controls */&lt;/span&gt;
    &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;.unit&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;nagi-css/owned-surface-reach-in&lt;/code&gt; rule reports the attempt to style the child's private DOM. To change its internals, edit the child's CSS or use an input it explicitly exposes, such as a prop or CSS custom property.&lt;/p&gt;

&lt;p&gt;Owning both source files does not give the parent a spare key to the child's DOM.&lt;/p&gt;

&lt;p&gt;This is also why I agree with Tailwind's recommendation to &lt;a href="https://v3.tailwindcss.com/docs/reusing-styles#compared-to-css-abstractions" rel="noopener noreferrer"&gt;reuse complex UI through components&lt;/a&gt;. In Nagi, the markup and its styles travel together, while the parent depends only on the child's root and exposed inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much naming remains?
&lt;/h2&gt;

&lt;p&gt;This small example needed no new words for its base classes. To see how much vocabulary a broader set of UI components would need, I examined &lt;a href="https://github.com/nagi-labs/nagi-ui" rel="noopener noreferrer"&gt;Nagi UI&lt;/a&gt;, an experimental library of common UI components I built under the Nagi CSS contract. I chose Vue because its templates keep the HTML directly visible.&lt;/p&gt;

&lt;p&gt;Using Nagi CSS 0.4.0, I measured &lt;strong&gt;31 component source files&lt;/strong&gt; in Nagi UI's documentation site. &lt;strong&gt;All 555 explicit base-class occurrences required no new vocabulary.&lt;/strong&gt; Their names either followed directly from the contract or came from its bounded anatomy vocabulary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;How the base class was named&lt;/th&gt;
&lt;th&gt;Occurrences&lt;/th&gt;
&lt;th&gt;Share of base classes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Derived directly from the contract&lt;/td&gt;
&lt;td&gt;426&lt;/td&gt;
&lt;td&gt;76.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Selected from the anatomy vocabulary&lt;/td&gt;
&lt;td&gt;129&lt;/td&gt;
&lt;td&gt;23.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total requiring no new vocabulary&lt;/td&gt;
&lt;td&gt;555&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The anatomy choices used just four words: &lt;code&gt;actions&lt;/code&gt;, &lt;code&gt;icon&lt;/code&gt;, &lt;code&gt;text&lt;/code&gt;, and &lt;code&gt;value&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Alongside the 555 base-class occurrences were &lt;strong&gt;115 author-named variant occurrences&lt;/strong&gt;, making up &lt;strong&gt;17.2% of the 670 class occurrences in total&lt;/strong&gt;. Like &lt;code&gt;-bold&lt;/code&gt; and &lt;code&gt;-italic&lt;/code&gt; in the opening example, these name stable distinctions. Nagi checks their form, ordering, and placement, but the author chooses the words.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/nagi-labs/nagi-ui/blob/main/docs/evaluations/naming-derivation.md" rel="noopener noreferrer"&gt;methodology&lt;/a&gt; and &lt;a href="https://github.com/nagi-labs/nagi-ui/blob/main/docs/evaluations/naming-derivation.json" rel="noopener noreferrer"&gt;source data&lt;/a&gt; are public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the agent write; review the decisions
&lt;/h2&gt;

&lt;p&gt;In this workflow, an AI agent writes most of the markup and CSS, following the repository's &lt;a href="https://github.com/nagi-labs/nagi-css/tree/main/skills/nagi-css" rel="noopener noreferrer"&gt;agent skill&lt;/a&gt;. When moving an element changes its selector path, the agent updates that path too, then runs the linter to check conformance. The structural coupling remains, but maintaining those paths becomes part of the agent's work.&lt;/p&gt;

&lt;p&gt;The human review can then concentrate on the decisions a rule cannot settle: whether the HTML semantics are correct, whether the component boundary makes sense, whether an anatomy or variant word expresses the intended role, and whether the result is accessible and visually correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the example, then change it
&lt;/h2&gt;

&lt;p&gt;To make the example executable, the repository includes a concrete Vue implementation in &lt;a href="https://github.com/nagi-labs/nagi-css/tree/main/examples/vue-minimal" rel="noopener noreferrer"&gt;&lt;code&gt;examples/vue-minimal&lt;/code&gt;&lt;/a&gt;, with state, click handlers, a parent component, and design-token values.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stackblitz.com/fork/github/nagi-labs/nagi-css/tree/main/examples/vue-minimal?startScript=dev" rel="noopener noreferrer"&gt;Open it in StackBlitz&lt;/a&gt;, or run it locally with Node.js 22.18+ and the example's pinned pnpm version, 11.1.3:&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;--depth&lt;/span&gt; 1 https://github.com/nagi-labs/nagi-css.git
&lt;span class="nb"&gt;cd &lt;/span&gt;nagi-css/examples/vue-minimal
pnpm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt;
pnpm lint
pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an independent project that installs the published plugin. You do not need to build Nagi CSS itself.&lt;/p&gt;

&lt;p&gt;Toggle the buttons and watch their appearance and status message change. Then try one intentional mistake. In &lt;code&gt;src/format-controls.vue&lt;/code&gt;, find the Bold button and change &lt;code&gt;button&lt;/code&gt; to &lt;code&gt;control&lt;/code&gt; in its class attribute, keeping &lt;code&gt;-bold&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- class="button -bold"
&lt;/span&gt;&lt;span class="gi"&gt;+ class="control -bold"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;pnpm lint&lt;/code&gt;. The &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; tag determines the class &lt;code&gt;button&lt;/code&gt;, so Nagi reports the mismatch. Change &lt;code&gt;control&lt;/code&gt; back to &lt;code&gt;button&lt;/code&gt; to restore the passing example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the same checks in your project
&lt;/h2&gt;

&lt;p&gt;The package is the same across the supported integrations. Install the plugin first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; @nagi-labs/eslint-plugin-nagi-css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep your framework's ESLint configuration and append Nagi CSS. The minimal configuration below uses Vue because it matches the runnable example:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pluginVue&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eslint-plugin-vue&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;nagiCss&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nagi-labs/eslint-plugin-nagi-css&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="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;pluginVue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flat/essential&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="nx"&gt;nagiCss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recommended&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;surfaceRootPrefixes&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="s2"&gt;app-&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;The prefix is the same &lt;code&gt;app-&lt;/code&gt; used throughout the article. Preserve any additional TypeScript and framework-specific settings in your project. There are &lt;a href="https://github.com/nagi-labs/nagi-css/tree/main/docs/getting-started" rel="noopener noreferrer"&gt;setup guides for Vue, Nuxt, Svelte, and Astro&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To give your coding agent the Nagi CSS workflow, install the skill from your application repository's root with the &lt;a href="https://skills.sh/docs/cli" rel="noopener noreferrer"&gt;skills CLI&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;npx skills add nagi-labs/nagi-css &lt;span class="nt"&gt;--skill&lt;/span&gt; nagi-css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer detects supported coding agents and installs the skill for the current project. Restart the agent session after installation so it can discover the new instructions. If your agent does not support Agent Skills, use Nagi CSS's &lt;a href="https://github.com/nagi-labs/nagi-css/blob/main/AGENTS.md" rel="noopener noreferrer"&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/a&gt; as the compact fallback.&lt;/p&gt;

&lt;p&gt;For the complete rules and their scope, see the &lt;a href="https://github.com/nagi-labs/nagi-css/blob/main/CONTRACT.md" rel="noopener noreferrer"&gt;full contract&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I would like to hear where this helps in your own components, and where it gets in the way. In particular: which naming choices would it remove for you, and which real refactor would make the structural rules too costly?&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgements
&lt;/h2&gt;

&lt;p&gt;Nagi CSS would not exist in its current form without &lt;a href="https://ricostacruz.com/rscss/" rel="noopener noreferrer"&gt;RSCSS&lt;/a&gt; by Rico Sta. Cruz.&lt;/p&gt;

&lt;p&gt;RSCSS showed me that a small set of conventions—thinking in components, naming elements locally, and using direct-child selectors to protect component boundaries—could make CSS dramatically easier to reason about.&lt;/p&gt;

&lt;p&gt;Nagi CSS takes those ideas in a more mechanically enforceable direction, deriving names from HTML and checking ownership boundaries statically. But its starting point is unmistakably RSCSS.&lt;/p&gt;

&lt;p&gt;Thank you, Rico, for publishing an approach that has shaped how I think about CSS for years.&lt;/p&gt;

</description>
      <category>css</category>
      <category>eslint</category>
      <category>html</category>
      <category>a11y</category>
    </item>
    <item>
      <title>How close can explicit HTTP endpoints get to server-function DX?</title>
      <dc:creator>Yoshinori Ishii</dc:creator>
      <pubDate>Sat, 05 Sep 2026 19:55:43 +0000</pubDate>
      <link>https://dev.to/yoshinoriishii/how-close-can-explicit-http-endpoints-get-to-server-function-dx-5b6c</link>
      <guid>https://dev.to/yoshinoriishii/how-close-can-explicit-http-endpoints-get-to-server-function-dx-5b6c</guid>
      <description>&lt;p&gt;Next.js, SolidStart, TanStack Start, and SvelteKit all offer some form of server or remote functions. The integrations differ, but being able to connect server code to forms and data fetching is appealing.&lt;/p&gt;

&lt;p&gt;I've used Nuxt since its early releases, and I like Nuxt 5's work on typed HTTP routes. Seeing those approaches develop made me wonder:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How much of that DX can we get while keeping an explicit HTTP endpoint as the starting point?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm trying this in &lt;a href="https://github.com/nuxt-endpoints/nuxt-endpoints" rel="noopener noreferrer"&gt;Nuxt Endpoints&lt;/a&gt;. Routes keep their files, URLs, and HTTP methods. NE's &lt;code&gt;defineRouteHandler()&lt;/code&gt; builds on the validated-routing design being discussed in &lt;a href="https://github.com/h3js/h3/issues/1437" rel="noopener noreferrer"&gt;H3 RFC #1437&lt;/a&gt;: request and response schemas alongside the handler, with a contract that other tools can read. NE implements and extends that design to connect it to client behavior, including pagination and forms. The upstream design is still under discussion; these examples use NE's implementation.&lt;/p&gt;

&lt;p&gt;The closest existing comparison is oRPC, which already combines type-safe procedures, explicit OpenAPI routing, and Pinia Colada integration across frameworks. Nuxt Endpoints explores a more Nuxt-native trade-off: keeping file-based routes as the source of truth, avoiding a parallel procedure router, and deriving behavior such as cursor pagination directly from semantic HTTP contracts.&lt;/p&gt;

&lt;p&gt;The idea is that a server contract should do more than type the response. It should constrain the handler and let the client use features that depend on that contract. A pagination adapter, for instance, should require a route that declares pagination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pagination: what the declaration buys you
&lt;/h2&gt;

&lt;p&gt;Cursor pagination is a small example of the idea:&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/articles.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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineRouteHandler&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-endpoints/runtime&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;listArticles&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/articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// your database code&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Article&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;id&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;number&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;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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineRouteHandler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cursor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Article&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;handler&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;listArticles&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;validated&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This defines &lt;code&gt;GET /api/articles&lt;/code&gt;. The database query and cursor encoding are your code. NE supplies the rest of the pagination contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the request has validated &lt;code&gt;cursor&lt;/code&gt; and &lt;code&gt;limit&lt;/code&gt; query fields;&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;200&lt;/code&gt; response is &lt;code&gt;{ items: Article[], nextCursor?: string }&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the handler's successful return must match that shape;&lt;/li&gt;
&lt;li&gt;the generated OpenAPI document exposes those fields;&lt;/li&gt;
&lt;li&gt;the client can use an adapter that requires this contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Returning &lt;code&gt;{ nextCursor }&lt;/code&gt; without &lt;code&gt;items&lt;/code&gt;, for example, is a server-side type error. This page format is NE's convention, not an HTTP standard. The contract checks the shape; it cannot prove that your database query returns the right next page.&lt;/p&gt;

&lt;p&gt;The client can pass the same typed request to Pinia Colada:&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;useInfiniteQuery&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;@pinia/colada&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;infiniteQueryOptions&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;#endpoints/colada&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;articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useInfiniteQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;infiniteQueryOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;$endpoint&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/articles&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;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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;infiniteQueryOptions()&lt;/code&gt; rejects an endpoint without the cursor-pagination contract at compile time. Nuxt Endpoints connects &lt;code&gt;nextCursor&lt;/code&gt; to the next HTTP request; Pinia Colada owns the page cache, reactive state, and refetch lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle responses by status
&lt;/h2&gt;

&lt;p&gt;For a detail route declaring &lt;code&gt;200&lt;/code&gt; with an article and &lt;code&gt;404&lt;/code&gt; with a message, checking the status narrows the body type:&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;$endpoint&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/articles/:id&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;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;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;42&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&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;body&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="k"&gt;if &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;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;404&lt;/code&gt; is a result you can handle, not a thrown fetch error. Network failures still reject the request. These types describe the declared responses; they cannot account for every response a proxy or other infrastructure might return.&lt;/p&gt;

&lt;p&gt;The same route is callable from another service or with &lt;code&gt;curl&lt;/code&gt;. Its HTTP interface is documented in the generated OpenAPI, so callers don't need NE to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forms, from the same contract
&lt;/h2&gt;

&lt;p&gt;The experimental Nuxt 5 prototype applies the same idea to progressively enhanced forms. The server is still an explicit POST 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="c1"&gt;// server/api/users.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;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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineRouteHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formOf&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-endpoints/runtime&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;UserInput&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;name&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;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="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;defineRouteHandler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;form&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users/new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users/{id}&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;validate&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="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="nx"&gt;UserInput&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/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;formOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UserInput&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="mi"&gt;201&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;id&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;number&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="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="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;handler&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="o"&gt;=&amp;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;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&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="na"&gt;name&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;validated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Vue page:&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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEndpointForm&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/users&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;name&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="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;form&lt;/span&gt; &lt;span class="na"&gt;v-bind=&lt;/span&gt;&lt;span class="s"&gt;"form.attrs"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;submit=&lt;/span&gt;&lt;span class="s"&gt;"form.enhance"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;
      Name
      &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;v-bind=&lt;/span&gt;&lt;span class="s"&gt;"form.fields.name"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"issue in form.issues.name"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"issue.message"&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;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;button&amp;gt;&lt;/span&gt;Create&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;/&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;&lt;code&gt;useEndpointForm&lt;/code&gt; requires a compatible form contract. It checks body values and field names against the input schema, so a field such as &lt;code&gt;form.fields.email&lt;/code&gt; would be a type error here. Build-time checks reject requirements that a native form cannot satisfy, such as a mandatory custom request header.&lt;/p&gt;

&lt;p&gt;NE generates the form attributes and connects server validation issues to &lt;code&gt;form.issues&lt;/code&gt;. The &lt;a href="https://github.com/nuxt-endpoints/nuxt-endpoints/blob/nuxt5/docs/progressive-enhancement.md" rel="noopener noreferrer"&gt;progressive-enhancement documentation&lt;/a&gt; covers the generated submission route and the browser tests with and without JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is available today?
&lt;/h2&gt;

&lt;p&gt;The published Nuxt 4 version includes pagination, status-aware requests, Pinia Colada adapters, OpenAPI generation, and idempotency. These are ported into the module using its own implementation.&lt;/p&gt;

&lt;p&gt;The Nuxt 5 branch is where I'm experimenting with upstream integration and progressive enhancement. It requires prototype forks. The extensions in those forks have not been accepted upstream.&lt;/p&gt;

&lt;p&gt;I'm following Nuxt's merged &lt;a href="https://github.com/nuxt/nuxt/pull/36238" rel="noopener noreferrer"&gt;fetchdts route-typing PR&lt;/a&gt;, which generates &lt;code&gt;$fetch&lt;/code&gt; and &lt;code&gt;useFetch&lt;/code&gt; types from reported server routes, Nitro's &lt;a href="https://github.com/nitrojs/nitro/pull/4572" rel="noopener noreferrer"&gt;removal of its previous typed-fetch implementation&lt;/a&gt;, and the &lt;a href="https://github.com/nitrojs/nitro/releases/tag/v3.0.260903-beta" rel="noopener noreferrer"&gt;September Nitro 3 beta&lt;/a&gt;. I'd like to contribute the contract introspection and type-inference pieces that downstream tools need, and reuse them in NE. The aim is less duplicated code while keeping NE's application APIs as stable as practical.&lt;/p&gt;

&lt;p&gt;You can see the current split and setup instructions on the &lt;a href="https://nuxt-endpoints.github.io/nuxt-endpoints/docs/nuxt5-progress" rel="noopener noreferrer"&gt;Nuxt 5 progress page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not make every remote operation a function?
&lt;/h2&gt;

&lt;p&gt;Server functions can expose HTTP controls, and these frameworks also support explicit API routes. I prefer starting with the route itself.&lt;/p&gt;

&lt;p&gt;For a simple GET, I want to write a simple GET. When I need pagination or form integration, I want to opt into that abstraction for that purpose. I can see the extra requirements in the declaration and inspect the resulting HTTP interface in OpenAPI.&lt;/p&gt;

&lt;p&gt;NE still introduces conventions: &lt;code&gt;pagination&lt;/code&gt; and &lt;code&gt;form&lt;/code&gt; are examples. That's fine with me as long as I can see what they add and call the endpoint without the generated client.&lt;/p&gt;

&lt;p&gt;These examples don't cover everything server functions offer. NE doesn't yet provide automatic cache revalidation after a mutation, for example. What I've been able to build so far makes me want to keep exploring this approach.&lt;/p&gt;

&lt;p&gt;What part of server-function DX would be hardest—or most valuable—to reproduce this way?&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nuxt-endpoints.github.io/nuxt-endpoints/" rel="noopener noreferrer"&gt;Nuxt Endpoints documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nuxt-endpoints/nuxt-endpoints" rel="noopener noreferrer"&gt;Nuxt Endpoints on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nuxt-endpoints/nuxt-endpoints/blob/main/docs/pagination-demo.md" rel="noopener noreferrer"&gt;Cursor pagination walkthrough&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nuxt-endpoints/nuxt-endpoints/tree/nuxt5#try-the-nuxt-5-integration-branch" rel="noopener noreferrer"&gt;Try the Nuxt 5 integration branch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nuxt</category>
      <category>typescript</category>
      <category>vue</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
