このページは英語でのみご覧いただけます。
Nuxt
Pathruleルール5件 • メモリ2件 • スキル1件
An opinionated bundle for Nuxt 4 on Vue 3 that keeps agents on the current API surface. It covers when to use useFetch, useAsyncData, and $fetch, how to write Nitro server routes, and how to keep secrets and client-only code out of the universal render. Each piece lands on the directory it governs, so the rules about server routes never leak into component code.
推奨されるパスの配置
Pathruleは各要素を対応するパスに配置するため、アシスタントはそれが属する場所でだけ参照します。これはインポート時に適用されるスコープで、ワークスペースで調整できます。
/ ワークスペースのルート
nuxt-review
app/
Fetch initial render data with useFetch or useAsyncData, never raw $fetch
Make useFetch URLs and keys reactive with refs or getters, not interpolated strings
Guard browser-only APIs; component setup runs on the server too
Nuxt 4 directory layout: app/ is the new srcDir
Auto-imports and shallowRef reactivity in Nuxt 4
server/
Write Nitro handlers with defineEventHandler and method-suffixed filenames
Keep secrets in runtimeConfig and read them with useRuntimeConfig(event)
ルール
5Fetch initial render data with useFetch or useAsyncData, never raw $fetch/app高強制Initial component data must go through the SSR-aware composables so the server payload transfers to the client.
| 1 | Any data that feeds the initial render must use `useFetch` or `useAsyncData`. These run once on the server and serialize the result into the payload so the client reuses it during hydration. Raw `$fetch` in `<script setup>` runs again on the client after hydration, causing a double fetch, wasted requests, and hydration issues. |
| 2 | |
| 3 | - Use `useFetch(url)` for a plain GET against one endpoint. It keys on the URL and method automatically and infers response types from `server/api`. |
| 4 | - Use `useAsyncData(key, fn)` when the work is not a simple endpoint call: wrapping an SDK, combining several requests, or reshaping the payload. Pass an explicit unique `key` so shared calls dedupe correctly. |
| 5 | - Reserve bare `$fetch` for client-only event handlers (form submit, button click) where SSR transfer does not apply. |
| 6 | - Do not pass `$fetch` as the `useFetch` handler (`useFetch(() => $fetch(url))`). Wrap custom `$fetch` logic in `useAsyncData(key, () => $fetch(...))` instead so caching and dedup still work. |
| 7 | - Errors are not thrown by these composables; read `error` and `status` to render error UI. Only bare `$fetch` throws on a non-2xx response. |
Make useFetch URLs and keys reactive with refs or getters, not interpolated strings/app高強制Dynamic data must use a ref/computed/getter URL or key so it refetches and stays hydration-consistent.
| 1 | When a fetch depends on a reactive value (route param, selected id, search box), pass the URL as a ref, computed, or getter function, not as an eagerly interpolated string. A plain template string is resolved once when the function runs and will not refetch when the value changes, and a `computed` used directly as the key or query is a known cause of hydration-mismatch warnings because the server and client hash it differently. |
| 2 | |
| 3 | - Pass a getter URL: `useFetch(() => \`/api/users/${id.value}\`)`. It recomputes and refetches when `id` changes. |
| 4 | - Reactive option values (`query`, `params`, `headers`) trigger a refetch when they change; the `watch` option re-runs the handler but does not rebuild the URL, so put dynamic parts in the URL getter. |
| 5 | - Give every `useAsyncData` an explicit unique `key`; do not rely on the auto-generated key for anything you fetch in more than one place. |
| 6 | - When the same key is reused across components, keep `handler`, `transform`, `pick`, `default`, `deep`, and `getCachedData` identical, or Nuxt warns and behavior diverges. |
| 7 | - Use `lazy: true` (or `useLazyFetch`) only when the data is not needed for the first paint or SEO; otherwise let it block navigation so the markup is complete on the server. |
Write Nitro handlers with defineEventHandler and method-suffixed filenames/server高強制One defineEventHandler per file, method in the filename, inputs via h3 helpers, errors via createError.
| 1 | Files in `server/api` are auto-mounted under `/api`; files in `server/routes` mount at the root. Export exactly one `defineEventHandler` per file. |
| 2 | |
| 3 | - Encode the HTTP method in the filename (`users.get.ts`, `users.post.ts`, `users.[id].delete.ts`) instead of branching on `event.method` inside one handler. |
| 4 | - Read inputs with the `h3` helpers: `getQuery(event)`, `getRouterParam(event, 'id')`, and `await readBody(event)`. Do not parse the raw request stream yourself. |
| 5 | - Throw `createError({ statusCode, statusMessage, data })` for failures so Nitro returns a structured error response with the right status code; do not return ad-hoc `{ error: ... }` objects with a 200. |
| 6 | - Keep handlers serializable in and out; return plain objects/arrays, not class instances or functions. |
Keep secrets in runtimeConfig and read them with useRuntimeConfig(event)/server高強制Private keys live in the private runtimeConfig block, never in public, and are read server-side with the event.
| 1 | `runtimeConfig` has two halves: top-level keys are server-only, and `runtimeConfig.public` is shipped to the browser in the payload. Anything secret (API tokens, DB URLs, signing keys) must be a top-level key, never under `public`. |
| 2 | |
| 3 | - Define `runtimeConfig: { apiSecret: '', public: { apiBase: '' } }` in `nuxt.config.ts`; only put values you are willing to expose in the page source under `public`. |
| 4 | - Override at runtime with `NUXT_*` env vars (`NUXT_API_SECRET`, `NUXT_PUBLIC_API_BASE`). Do not read `process.env` directly in app or server code; values must round-trip through `runtimeConfig`. |
| 5 | - In server handlers call `useRuntimeConfig(event)`, passing `event`, so per-request env overrides apply. Calling it without `event` in a handler can miss runtime overrides. |
| 6 | - Never reference a top-level (private) runtimeConfig key inside a component or composable that runs on the client; it will be `undefined` there and signals the secret was nearly leaked. |
Guard browser-only APIs; component setup runs on the server too/app高強制window, document, localStorage and similar must be guarded for SSR using import.meta.client or onMounted.
| 1 | Nuxt 4 renders universally by default, so `<script setup>` and composables execute on the server during SSR. Touching browser globals there throws or produces server/client divergence. |
| 2 | |
| 3 | - Wrap `window`, `document`, `localStorage`, `navigator`, and DOM measurements in `if (import.meta.client) { ... }` or run them inside `onMounted`, which only fires on the client. |
| 4 | - Use `import.meta.server` / `import.meta.client` for branch logic. The legacy `process.server` / `process.client` flags are removed in Nuxt 4. |
| 5 | - Do not derive rendered text from values that differ between server and client (timestamps, `Math.random`, locale-dependent formatting) without `<ClientOnly>` or a mounted guard; that is a classic hydration-mismatch source. |
| 6 | - For genuinely client-only widgets, wrap them in `<ClientOnly>` and provide a `fallback` so the server output stays stable. |
メモリ
2Nuxt 4 directory layout: app/ is the new srcDir/appWhere source lives after the Nuxt 4 move to app/, and what stays at the project root.
| 1 | Nuxt 4 changed the default `srcDir` to `app/`. Vue source moved under `app/` (`app/pages`, `app/components`, `app/composables`, `app/layouts`, `app/plugins`, `app/middleware`, plus `app/app.vue`), while several directories deliberately stay at the project root. |
| 2 | |
| 3 | - Root-level (not under `app/`): `server/`, `modules/`, `layers/`, `shared/`, `public/`, `content/`, and `nuxt.config.ts`. |
| 4 | - Put API handlers in `server/api`, Nitro middleware/plugins in `server/`, and code that must run in BOTH the Vue app and the Nitro server in `shared/` (it is auto-imported on both sides). |
| 5 | - This is a frequent source of confusion when porting Nuxt 2/3 code or following old tutorials that assume a flat root or a `src/` dir. If migrating an existing project, you can keep the old layout by setting `srcDir` and `dir` overrides, but new code should follow the `app/` default. |
| 6 | |
| 7 | See /server for the Nitro handler and runtimeConfig secret conventions. |
Auto-imports and shallowRef reactivity in Nuxt 4/appWhat is auto-imported, and why fetched data needs whole-value replacement to stay reactive.
| 1 | Two Nuxt 4 conventions change how everyday component code is written. |
| 2 | |
| 3 | Auto-imports: composables, components, and exports from `app/composables` and `app/utils` are auto-imported. Do not add manual `import` lines for them; a duplicate manual import can shadow the auto-import. `$fetch` is the global auto-imported alias for `ofetch`, and Vue APIs (`ref`, `computed`, `watch`, `onMounted`) are auto-imported too. |
| 4 | |
| 5 | shallowRef data: in Nuxt 4 the `data` returned by `useFetch` / `useAsyncData` is a `shallowRef`, not a deeply reactive ref. This is a deliberate performance choice that avoids tracking every nested property. The practical consequence: mutating a nested field in place (`data.value.items.push(x)`) will not reliably trigger reactivity. Replace the whole value (`data.value = { ...data.value, items: [...] }`) or call the composable's `refresh()`. If you genuinely need deep reactivity for one call, opt in with `deep: true`. |
スキル
1nuxt-review/rootPre-merge checklist for Nuxt 4 data fetching, server routes, secrets, and SSR safety.
| 1 | --- |
| 2 | name: nuxt-review |
| 3 | description: Review a Nuxt 4 (Vue 3 + Nitro) change for correct data fetching, reactive keys, server route conventions, runtimeConfig secret handling, and SSR-safe code before merging. |
| 4 | --- |
| 5 | |
| 6 | # Nuxt 4 review |
| 7 | |
| 8 | ## Data fetching (app/) |
| 9 | - [ ] Initial render data uses `useFetch` or `useAsyncData`, not raw `$fetch` in `<script setup>`. |
| 10 | - [ ] `$fetch` appears only in client event handlers (submit, click), never for first-paint data. |
| 11 | - [ ] Dynamic URLs are getters/refs/computed, not eagerly interpolated strings. |
| 12 | - [ ] No bare `computed` passed as a `useFetch` key or query (hydration-mismatch risk). |
| 13 | - [ ] Every `useAsyncData` call has an explicit unique `key`; shared keys keep `transform`/`pick`/`default` identical. |
| 14 | - [ ] `error` and `status` are handled in the template (these composables do not throw). |
| 15 | - [ ] `lazy: true` / `useLazyFetch` used only when the data is not needed for first paint or SEO. |
| 16 | - [ ] Nested fetched data is replaced wholesale or refreshed, not mutated in place (shallowRef). |
| 17 | |
| 18 | ## Server routes & secrets (server/) |
| 19 | - [ ] One `defineEventHandler` per file; method encoded in the filename (`*.get.ts`, `*.post.ts`). |
| 20 | - [ ] Inputs read via `getQuery`, `getRouterParam`, `await readBody`; errors via `createError`. |
| 21 | - [ ] Secrets live as top-level `runtimeConfig` keys, never under `runtimeConfig.public`. |
| 22 | - [ ] Server handlers read config with `useRuntimeConfig(event)` (event passed). |
| 23 | - [ ] No private runtimeConfig key or secret referenced in client-side component/composable code. |
| 24 | |
| 25 | ## SSR safety (app/) |
| 26 | - [ ] Browser-only APIs (`window`, `document`, `localStorage`) guarded with `import.meta.client` or `onMounted`. |
| 27 | - [ ] No `process.server` / `process.client` (use `import.meta.server` / `import.meta.client`). |
| 28 | - [ ] Server/client-divergent values (`Date`, random, locale) are wrapped in `<ClientOnly>` or a mounted guard. |
| 29 | - [ ] No manual imports added for auto-imported composables, components, or utils. |
このパターンを使う理由
AI agents reach for stale Nuxt 2 and 3 habits, misuse the data-fetching composables, and leak secrets or break hydration in the universal render.
対象:Teams building full-stack Nuxt 4 apps on Vue 3 with Nitro
アシスタントが次のことをしないようにします。
- Calling raw $fetch for initial page data, causing double fetches and hydration mismatches
- Reading server-only secrets through runtimeConfig.public or env vars on the client
- Touching window, document, or localStorage during SSR without a client guard
- ライセンス
- Apache-2.0
- バージョン
- 1.0.1
- 更新日
- 2026-08-25