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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- nuxt-4 Installs to ~/.agents/skills/nuxt-4/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install nuxt-4 The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project nuxt-4 Installs to .agents/skills/nuxt-4/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/nuxt-4.zipunzip -o nuxt-4.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) nuxt-4 PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- Plain Vue component and composable design with no Nuxt specifics: use vue-3-composition
- Store architecture and store tests: use pinia-state
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.
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
- Read
AGENTS.md,package.json(nuxtversion) andnuxt.config.ts:ssr,routeRules,modules,runtimeConfig,compatibilityVersion,srcDir. - 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
| Need | Use |
|---|---|
| Page/component data on load, SSR-friendly | useFetch('/api/x') |
| Data from an SDK, DB client or multiple calls | useAsyncData('key', () => ...) |
| Fetch in an event handler (click, submit) | $fetch |
| Fetch only in the browser | useFetch(url, { server: false }) |
| Do not block navigation | { lazy: true } and render status === 'pending' |
| Re-fetch when a ref changes | Reactive URL/query (query: { page }) or watch: [page] |
| Invalidate after a mutation | refresh() 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:
datais ashallowRef. Replace it, do not mutate nested fields expecting re-render (or passdeep: truefor that call).- Without
default,dataanderrorstart asundefined(Nuxt 3 usednull). Checkstatus(idle | pending | success | error) rather than truthiness. - Calls with the same key share one
data,errorandstatus. Options that change the result (transform,pick,default,getCachedData) must match across those calls. useFetchbuilds the key from URL and options; foruseAsyncDataalways pass an explicit key that includes every input (`product-${id}`).- Top-level
$fetchinsetupruns on the server and again on the client. That is the classic double fetch; useuseFetch/useAsyncData. - During SSR,
$fetch('/api/...')calls the handler in-process without the browser’s cookies.useFetchforwards request headers; insideuseAsyncDatauseuseRequestFetch()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); onlyruntimeConfig.publicreaches the browser. Override at runtime withNUXT_env vars (NUXT_API_SECRET). - Never import from
server/inapp/. Put shared types and pure helpers inshared/. - Auth on every handler that touches user data; route middleware in
app/middlewareonly 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 } },
},
});
| Content | Rule |
|---|---|
| Marketing, docs, rarely changes | prerender: true (or nuxi generate for the whole site) |
| Public, changes hourly, many pages | isr on Vercel/Netlify, swr elsewhere |
| Personalised per user | default SSR, no shared cache |
| Behind login, heavy client interaction, no SEO | ssr: 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-levelrefin 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/localStoragereads in setup, and invalid HTML nesting (<div>inside<p>). Fix by computing once on the server and storing inuseState, moving browser reads intoonMounted, or wrapping in<ClientOnly>with a fallback of the same size. - Use
useId()for ids linking labels and inputs, never counters. - Components named
*.client.vuerender only in the browser;*.server.vuerender 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 ... mismatchwarnings. curl -iprerendered and SWR routes to confirm cache headers;curl -X PATCHa 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.yaml
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.