Nuxt 4 skill for Codex

Fetch once, render per route, hydrate clean. Codex works inside the Nuxt 4 layout of app/, server/ and shared/, fetches with keyed useFetch or useAsyncData so data travels in the SSR payload instead of being requested twice, validates every Nitro handler's input, and picks prerender, ISR, SWR or client-only rendering per route. It also walks a Nuxt 3 project through the v4 data-fetching changes that break null checks.

  • Frontend
  • $nuxt-4
  • 181 lines
  • 2 files
  • ~2,000 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- nuxt-4

Installs to ~/.agents/skills/nuxt-4/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $nuxt-4, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds and upgrades Nuxt 4 apps - the app/ source directory, shared/ and server/ layout, useFetch and useAsyncData keys with shallow data and status, Nitro server routes with validated input, useState for SSR-safe state, routeRules for prerender, ISR, SWR and SPA sections, and hydration mismatch fixes. Use when adding pages, data fetching or API endpoints to a Nuxt project, choosing a rendering mode per route, migrating from Nuxt 3, or debugging double fetches and hydration warnings.

Use it when

  • Adding pages, data fetching or server API routes to a Nuxt app
  • Choosing prerender, SSR, ISR or SPA mode for sections of a site
  • Upgrading from Nuxt 3 to Nuxt 4
  • Hydration warnings or duplicate requests after page load

Use something else when

How to check the result

nuxi typecheck and nuxi build pass, a hard reload shows SSR data in the payload with no duplicate request, and the console has no hydration mismatch warnings.

The mistake it prevents

Putting swr or isr on a page that renders user-specific data, so one visitor's personalised HTML is cached and served to everyone.

~/.agents/skills/nuxt-4/SKILL.md

--- name: nuxt-4 description: Builds and upgrades Nuxt 4 apps - the app/ source directory, shared/ and server/ layout, useFetch and useAsyncData keys with shallow data and status,… license: MIT ---

Done well means: data is fetched once on the server and reused on the client (no double request), every fetch has a stable key, server routes validate their input and return typed responses, rendering is chosen per route in routeRules, and the page hydrates with zero mismatch warnings in the browser console.

Inspect first

  1. Read AGENTS.md, package.json (nuxt version) and nuxt.config.ts: ssr, routeRules, modules, runtimeConfig, compatibilityVersion, srcDir.
  2. Check the layout. Nuxt 4 default:
app/                 # srcDir: pages/, components/, composables/, layouts/,
                     # middleware/, plugins/, utils/, app.vue, app.config.ts, error.vue
server/              # Nitro: api/, routes/, middleware/, plugins/, utils/
shared/              # code imported by both app and server (utils/, types/ auto-imported)
public/
nuxt.config.ts

If pages/ sits at the root with no app/ directory, the project uses the Nuxt 3 layout. Do not move files as a side effect of another task. 3. Note the package manager from the lockfile and the deploy target (Vercel, Netlify, Cloudflare, Node) since it decides which routeRules features work.

Data fetching

NeedUse
Page/component data on load, SSR-friendlyuseFetch('/api/x')
Data from an SDK, DB client or multiple callsuseAsyncData('key', () => ...)
Fetch in an event handler (click, submit)$fetch
Fetch only in the browseruseFetch(url, { server: false })
Do not block navigation{ lazy: true } and render status === 'pending'
Re-fetch when a ref changesReactive URL/query (query: { page }) or watch: [page]
Invalidate after a mutationrefresh() from the call, or refreshNuxtData('key')
<script setup lang="ts">
const route = useRoute();
const page = computed(() => Number(route.query.page ?? 1));

const { data, status, error, refresh } = await useFetch("/api/products", {
  key: "products-list",
  query: { page },                      // reactive: refetches when page changes
  pick: ["items", "total"],             // only these fields go into the payload
  default: () => ({ items: [], total: 0 }),
});

async function remove(id: string) {
  await $fetch(`/api/products/${id}`, { method: "DELETE" });
  await refresh();
}
</script>

Nuxt 4 data-fetching behaviour to rely on:

  • data is a shallowRef. Replace it, do not mutate nested fields expecting re-render (or pass deep: true for that call).
  • Without default, data and error start as undefined (Nuxt 3 used null). Check status (idle | pending | success | error) rather than truthiness.
  • Calls with the same key share one data, error and status. Options that change the result (transform, pick, default, getCachedData) must match across those calls.
  • useFetch builds the key from URL and options; for useAsyncData always pass an explicit key that includes every input (`product-${id}`).
  • Top-level $fetch in setup runs on the server and again on the client. That is the classic double fetch; use useFetch/useAsyncData.
  • During SSR, $fetch('/api/...') calls the handler in-process without the browser’s cookies. useFetch forwards request headers; inside useAsyncData use useRequestFetch() when the endpoint needs the user’s session.

Server routes (Nitro)

// server/api/products/[id].patch.ts
import { z } from "zod";

const Body = z.object({ name: z.string().min(1).max(120), price: z.number().nonnegative() });

export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, "id");
  const user = await requireUser(event);                 // server/utils/auth.ts, auto-imported
  const body = await readValidatedBody(event, Body.parse);
  const product = await db.products.update({ id, ownerId: user.id, ...body });
  if (!product) throw createError({ statusCode: 404, statusMessage: "Product not found" });
  return product;                                        // JSON, typed for $fetch callers
});
  • Method suffixes (.get.ts, .post.ts, .patch.ts) route by HTTP method.
  • Use getValidatedQuery, readValidatedBody, getValidatedRouterParams; they throw a 400 on failure.
  • Read secrets with useRuntimeConfig(event); only runtimeConfig.public reaches the browser. Override at runtime with NUXT_ env vars (NUXT_API_SECRET).
  • Never import from server/ in app/. Put shared types and pure helpers in shared/.
  • Auth on every handler that touches user data; route middleware in app/middleware only protects navigation, not the API.

Rendering per route

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    "/": { prerender: true },
    "/blog/**": { isr: 3600 },         // platform cache, regenerated hourly (Vercel/Netlify)
    "/products/**": { swr: 600 },      // Nitro cache, stale-while-revalidate
    "/dashboard/**": { ssr: false },   // client-only SPA section
    "/api/public/**": { cors: true, headers: { "cache-control": "s-maxage=60" } },
    "/old-pricing": { redirect: { to: "/pricing", statusCode: 301 } },
  },
});
ContentRule
Marketing, docs, rarely changesprerender: true (or nuxi generate for the whole site)
Public, changes hourly, many pagesisr on Vercel/Netlify, swr elsewhere
Personalised per userdefault SSR, no shared cache
Behind login, heavy client interaction, no SEOssr: false

Never put swr/isr on a route whose HTML contains user-specific data: the first user’s page gets served to everyone.

State and hydration

  • Shared, SSR-safe state: useState('cart', () => []). Never a module-level ref in a composable; on the server it is shared across all requests.
  • Larger state: Pinia via @pinia/nuxt (see $pinia-state).
  • Hydration mismatches come from values that differ between server and client: Date.now(), Math.random(), locale formatting, window/localStorage reads in setup, and invalid HTML nesting (<div> inside <p>). Fix by computing once on the server and storing in useState, moving browser reads into onMounted, or wrapping in <ClientOnly> with a fallback of the same size.
  • Use useId() for ids linking labels and inputs, never counters.
  • Components named *.client.vue render only in the browser; *.server.vue render only on the server (server components, no client JS).

Upgrading from Nuxt 3

- [ ] Clean git tree; note current nuxt version and green test/build
- [ ] npx nuxi upgrade --dedupe        (needs network)
- [ ] Optional codemod: npx codemod@latest nuxt/4/migration-recipe
- [ ] Move app code into app/ (or set srcDir explicitly to keep the old layout)
- [ ] Audit data fetching: null checks -> status/undefined, nested mutation of data,
      shared keys with different options, getCachedData signature
- [ ] Run nuxi typecheck (separate app/server/shared TS projects catch new errors)
- [ ] Build, preview, click through SSR pages watching the console

Nuxt 3.12+ can preview v4 behaviour with future: { compatibilityVersion: 4 } before bumping the package. Ask before running codemods on a dirty tree.

Verification

npx nuxi typecheck
npx nuxi build && npx nuxi preview     # production server locally
npx vitest run                         # with @nuxt/test-utils if configured
  • Network tab on a hard reload: data for SSR pages arrives in the HTML payload; no duplicate XHR for the same endpoint after hydration.
  • Browser console: no Hydration ... mismatch warnings.
  • curl -i prerendered and SWR routes to confirm cache headers; curl -X PATCH a server route with an invalid body and get 400, without a session and get 401.

Output

List the routes and endpoints added or changed, the rendering rule for each, the data keys introduced, and any runtime config or env vars the deploy needs.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Nuxt 4"
  short_description: "Fetch once, render per route, hydrate clean"
  default_prompt: "$nuxt-4 Add a products API route and a paginated page that fetches it with useFetch"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem