@gallopsystems/agent-skills 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/plugins/nuxt-nitro-api/.claude-plugin/plugin.json +1 -1
  4. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/SKILL.md +24 -11
  5. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/auth-patterns.md +1 -0
  6. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/caching.md +68 -0
  7. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/composables-utils.md +32 -2
  8. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/error-handling.md +74 -0
  9. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/fetch-patterns.md +18 -0
  10. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/layers.md +40 -0
  11. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/route-rules.md +41 -0
  12. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/server-runtime.md +97 -0
  13. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/ssr-client.md +60 -0
  14. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/state-management.md +68 -0
  15. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/storage.md +59 -0
  16. package/plugins/volt-primevue/skills/volt-primevue/SKILL.md +56 -12
  17. package/plugins/volt-primevue/skills/volt-primevue/config.md +51 -0
  18. package/plugins/volt-primevue/skills/volt-primevue/gotchas.md +49 -0
  19. package/plugins/volt-primevue/skills/volt-primevue/theming.md +20 -6
  20. package/plugins/vue-nuxt/.claude-plugin/plugin.json +8 -0
  21. package/plugins/vue-nuxt/skills/vue-nuxt/SKILL.md +52 -0
  22. package/plugins/vue-nuxt/skills/vue-nuxt/auto-imports.md +48 -0
  23. package/plugins/vue-nuxt/skills/vue-nuxt/component-authoring.md +159 -0
  24. package/plugins/vue-nuxt/skills/vue-nuxt/composables.md +95 -0
  25. package/plugins/{nuxt-nitro-api/skills/nuxt-nitro-api → vue-nuxt/skills/vue-nuxt}/formatters.md +13 -1
  26. package/plugins/{nuxt-nitro-api/skills/nuxt-nitro-api → vue-nuxt/skills/vue-nuxt}/page-structure.md +1 -0
  27. package/plugins/vue-nuxt/skills/vue-nuxt/reactivity.md +133 -0
  28. package/plugins/vue-nuxt/skills/vue-nuxt/slots.md +139 -0
  29. package/plugins/vue-nuxt/skills/vue-nuxt/template-idioms.md +142 -0
  30. package/plugins/vue-nuxt/skills/vue-nuxt/v-model.md +106 -0
  31. package/plugins/vue-nuxt/skills/vue-nuxt/watch.md +194 -0
package/README.md CHANGED
@@ -19,6 +19,7 @@ Then install the skills you want:
19
19
  /plugin install git-github@gallop-systems-agent-skills
20
20
  /plugin install copier-template@gallop-systems-agent-skills
21
21
  /plugin install volt-primevue@gallop-systems-agent-skills
22
+ /plugin install vue-nuxt@gallop-systems-agent-skills
22
23
  ```
23
24
 
24
25
  ## Updating
@@ -87,14 +88,13 @@ Covers:
87
88
 
88
89
  ### nuxt-nitro-api
89
90
 
90
- Nuxt 3 / Nitro API patterns for building type-safe full-stack applications. Automatically activates when working in Nuxt 3 projects.
91
+ Nuxt 4 / Nitro API patterns for building type-safe full-stack applications. Automatically activates when working in Nuxt 4 projects.
91
92
 
92
93
  Covers:
93
94
  - Zod validation with h3 (Standard Schema support)
94
95
  - useFetch vs $fetch vs useAsyncData
95
96
  - Type inference (don't add manual types!)
96
97
  - nuxt-auth-utils (OAuth, WebAuthn, middleware)
97
- - Page structure (keep pages thin)
98
98
  - Composables vs utils
99
99
  - SSR + localStorage patterns
100
100
  - Deep linking (URL params sync)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gallopsystems/agent-skills",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "Gallop Systems Claude Code skills, symlinked into .claude/skills on install.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-nitro-api",
3
- "description": "Nuxt 3 / Nitro API patterns: validation, auth, SSR, tasks, SSE",
3
+ "description": "Nuxt 4 / Nitro API patterns: validation, auth, SSR, tasks, SSE",
4
4
  "version": "1.0.0",
5
5
  "author": {
6
6
  "name": "yeedle"
@@ -1,16 +1,22 @@
1
1
  ---
2
2
  name: nuxt-nitro-api
3
- description: Build type-safe Nuxt 3 applications with Nitro API patterns. Covers validation, fetch patterns, auth, SSR, composables, background tasks, and real-time features.
3
+ description: Build type-safe Nuxt 4 applications with Nitro API patterns. Covers validation, fetch patterns, auth, SSR, composables, background tasks, and real-time features.
4
4
  ---
5
5
 
6
- # Nuxt 3 / Nitro API Patterns
6
+ # Nuxt 4 / Nitro API Patterns
7
7
 
8
- This skill provides patterns for building type-safe Nuxt 3 applications with Nitro backends.
8
+ This skill provides patterns for building type-safe Nuxt applications with Nitro backends.
9
+
10
+ > **Version note:** Nitro's version follows Nuxt — **Nuxt 4.x ships Nitro v2**
11
+ > (`nitropack ^2.x`, via `@nuxt/nitro-server`; you have no direct `nitropack`
12
+ > dependency). `nitro.build` now documents **Nitro v3**, which renames core APIs;
13
+ > copying its examples verbatim breaks here. See [server-runtime.md](./server-runtime.md)
14
+ > for the v2↔v3 mapping.
9
15
 
10
16
  ## When to Use This Skill
11
17
 
12
18
  Use this skill when:
13
- - Working in a Nuxt 3 project with TypeScript
19
+ - Working in a Nuxt 4 project with TypeScript
14
20
  - Building API endpoints with Nitro
15
21
  - Implementing authentication with nuxt-auth-utils
16
22
  - Handling SSR + client-side state
@@ -21,13 +27,18 @@ Use this skill when:
21
27
  For detailed patterns, see these topic-focused reference files:
22
28
 
23
29
  - [validation.md](./validation.md) - Zod validation with h3, Standard Schema, error handling
24
- - [fetch-patterns.md](./fetch-patterns.md) - useFetch vs $fetch vs useAsyncData
30
+ - [fetch-patterns.md](./fetch-patterns.md) - useFetch vs $fetch vs useAsyncData, refreshNuxtData
25
31
  - [auth-patterns.md](./auth-patterns.md) - nuxt-auth-utils, OAuth, WebAuthn, middleware
26
- - [page-structure.md](./page-structure.md) - Keep pages thin, components do the work
27
- - [composables-utils.md](./composables-utils.md) - When to use composables vs utils
28
- - [formatters.md](./formatters.md) - Centralize currency/date/number formatters in useFormatters, never inline
29
- - [ssr-client.md](./ssr-client.md) - SSR + localStorage, hydration, VueUse
32
+ - [error-handling.md](./error-handling.md) - createError (fatal), error.vue, clearError, NuxtErrorBoundary
33
+ - [state-management.md](./state-management.md) - useState (never a module-scope ref), clearNuxtState, callOnce
34
+ - [composables-utils.md](./composables-utils.md) - composables vs utils, runtimeConfig public/private, runWithContext
35
+ - [ssr-client.md](./ssr-client.md) - SSR + localStorage, hydration, useRequestURL/Headers, useCookie, VueUse
30
36
  - [deep-linking.md](./deep-linking.md) - URL params sync with filters and useFetch
37
+ - [caching.md](./caching.md) - defineCachedFunction/EventHandler, SWR, per-key invalidation (Nitro v2)
38
+ - [storage.md](./storage.md) - useStorage / unstorage KV layer, mounts
39
+ - [route-rules.md](./route-rules.md) - declarative cache/headers/redirect/proxy/CORS per path
40
+ - [server-runtime.md](./server-runtime.md) - **Nitro v2 vs v3 version pin**, middleware order, useEvent, internal-$fetch auth, WebSockets
41
+ - [layers.md](./layers.md) - sharing components/composables/config across repos via extends
31
42
  - [nitro-tasks.md](./nitro-tasks.md) - Background jobs, scheduled tasks, job queues
32
43
  - [sse.md](./sse.md) - Server-Sent Events for real-time streaming
33
44
  - [server-services.md](./server-services.md) - Third-party service integration patterns
@@ -49,7 +60,7 @@ Working examples from a Nuxt project:
49
60
  2. **Use h3 validation** - `getValidatedQuery()`, `readValidatedBody()` with Zod schemas
50
61
  3. **Composables for context, utils for pure functions** - Composables access Nuxt context, utils are pure
51
62
  4. **SSR-safe code** - Guard browser APIs with `import.meta.client` or `onMounted`
52
- 5. **Keep pages thin** - Pages = layout + route params + components. Components own data fetching and logic.
63
+ 5. **Keep pages thin** - Pages = layout + route params + components. Components own data fetching and logic. (Page composition + display formatting now live in the `vue-nuxt` skill.)
53
64
 
54
65
  ## Auto-Imports Quick Reference
55
66
 
@@ -72,7 +83,7 @@ From nuxt-auth-utils:
72
83
  All auto-imported:
73
84
  - Vue: `ref`, `computed`, `watch`, `onMounted`, etc.
74
85
  - VueUse: `refDebounced`, `useLocalStorage`, `useUrlSearchParams`, etc.
75
- - Nuxt: `useFetch`, `useAsyncData`, `useRoute`, `useRouter`, `useState`, `navigateTo`
86
+ - Nuxt: `useFetch`, `useAsyncData`, `useRoute`, `useRouter`, `useState`, `navigateTo`, `callOnce`, `refreshNuxtData`, `clearNuxtState`, `useRequestURL`, `useRequestHeaders`, `useCookie`, `createError`, `showError`, `clearError`, `useNuxtApp`
76
87
 
77
88
  ### Shared (`/shared` directory - Nuxt 3.14+)
78
89
 
@@ -209,6 +220,8 @@ Needs Nuxt/Vue context (useRuntimeConfig, useRoute, refs)?
209
220
  8. **Cookie size limit is 4096 bytes** - Store only essential session data.
210
221
  9. **Ambiguous routes need type assertion** - See below.
211
222
  10. **Never use generic type params with useFetch/$fetch** - See below.
223
+ 11. **Never export a module-scope `ref` for shared state** - Leaks across SSR requests; use `useState`. See [state-management.md](./state-management.md).
224
+ 12. **Internal server `$fetch` doesn't forward cookies** - Pass `headers` explicitly or the callee sees no session. See [server-runtime.md](./server-runtime.md).
212
225
 
213
226
  ### Ambiguous Route Type Inference
214
227
 
@@ -226,3 +226,4 @@ NUXT_OAUTH_GOOGLE_CLIENT_SECRET=...
226
226
  4. **setUserSession merges** - Use `replaceUserSession` to replace
227
227
  5. **requireUserSession throws** - Use getUserSession for null
228
228
  6. **Cannot use with `nuxt generate`** - Requires running server
229
+ 7. **`navigateTo` external routes need `{ external: true }`** - In-app routes: `navigateTo('/dashboard')`. Server/OAuth endpoints that aren't Vue routes (e.g. `/api/auth/google`, `/api/auth/preview-login`) must pass `navigateTo('/api/auth/google', { external: true })`, or Vue Router tries to resolve them as client routes and fails. Build query routes with `navigateTo({ path, query })` and `String()` any numeric ids.
@@ -0,0 +1,68 @@
1
+ # Response & Function Caching (Nitro)
2
+
3
+ Nitro ships a KV-backed cache layer with stale-while-revalidate and per-key
4
+ invalidation. Reach for it before adding a memoization Map or a Postgres cache
5
+ table. All helpers are **auto-imported in `server/`**.
6
+
7
+ > **Nitro v2 names** (this project — see [server-runtime.md](./server-runtime.md)).
8
+ > `nitro.build` documents v3, which moves these to `import { … } from "nitro/cache"`.
9
+ > Here they are auto-imported globals.
10
+
11
+ ## `defineCachedFunction` — cache an expensive async function
12
+
13
+ ```typescript
14
+ const getStars = defineCachedFunction(
15
+ async (repo: string) => {
16
+ const { stargazers_count } = await $fetch(`https://api.github.com/repos/${repo}`);
17
+ return stargazers_count as number;
18
+ },
19
+ {
20
+ maxAge: 60 * 60, // serve from cache for 1h
21
+ swr: true, // after maxAge, serve stale and revalidate in the background
22
+ name: "ghStars",
23
+ group: "nitro/functions",
24
+ getKey: (repo) => repo, // cache key from the args
25
+ },
26
+ );
27
+
28
+ await getStars("unjs/nitro");
29
+ await getStars.invalidate("unjs/nitro"); // drop one key on demand (e.g. after a mutation)
30
+ ```
31
+
32
+ `getKey` is what makes it safe to call with different arguments — without it,
33
+ every arg shares one cache entry.
34
+
35
+ ## `defineCachedEventHandler` — cache a whole endpoint
36
+
37
+ ```typescript
38
+ export default defineCachedEventHandler(
39
+ async (event) => ({ now: Date.now(), data: await loadData() }),
40
+ {
41
+ maxAge: 60,
42
+ swr: true,
43
+ varies: ["host"], // vary the key on these headers
44
+ shouldBypassCache: (event) => !!getQuery(event).fresh, // skip cache for ?fresh=1
45
+ getKey: (event) => event.path,
46
+ },
47
+ );
48
+ ```
49
+
50
+ Don't cache an endpoint whose body depends on the session/user unless you `vary`
51
+ on the discriminator — otherwise one user's response is served to another.
52
+
53
+ ## Where the cache lives
54
+
55
+ The cache backs onto the `cache` **storage mount** (see [storage.md](./storage.md)):
56
+ filesystem in dev, whatever you mount (Redis, etc.) in production. Configure via
57
+ `nitro.storage` / `nitro.devStorage` in `nuxt.config.ts`.
58
+
59
+ ## Gotchas
60
+
61
+ - **`maxAge` is seconds, not ms.**
62
+ - **`swr: true`** means a request after expiry gets the *stale* value immediately
63
+ while a background refresh runs — great for latency, but readers can see
64
+ slightly old data. Omit it for must-be-fresh endpoints.
65
+ - **Invalidate on write.** After a mutation changes the source data, call
66
+ `fn.invalidate(key)` — caches don't know your DB changed.
67
+ - **Route-level caching** for simple cases can be declared without a handler
68
+ wrapper via `routeRules` (`swr` / `cache`) — see [route-rules.md](./route-rules.md).
@@ -82,7 +82,7 @@ export const usePermissions = () => {
82
82
  > **Formatters belong in one shared place.** The examples below show util *placement*,
83
83
  > not where to call formatters from. Never define a currency/date/number formatter inline
84
84
  > at the call site — centralize them in `useFormatters` or a shared util, and prefer
85
- > VueUse / date-fns over hand-rolling. See [formatters.md](./formatters.md).
85
+ > VueUse / date-fns over hand-rolling. See the `vue-nuxt` skill's `formatters.md`.
86
86
 
87
87
  ```typescript
88
88
  // utils/formatting.ts
@@ -161,6 +161,35 @@ export function formatCurrency(amount: number) {
161
161
  // - /pages/invoice.vue
162
162
  ```
163
163
 
164
+ ## `useRuntimeConfig`: `public` on the client, private on the server
165
+
166
+ Only `config.public.*` exists in client code. Reading a private (top-level) key
167
+ in the browser returns an empty string — a silent bug, not an error:
168
+
169
+ ```typescript
170
+ const config = useRuntimeConfig();
171
+ config.public.apiBase; // ✅ client + server
172
+ config.apiSecret; // ✅ server only — ❌ empty on the client
173
+ ```
174
+
175
+ Env binding follows the split: `NUXT_*` → server keys, `NUXT_PUBLIC_*` → public
176
+ keys. Put anything a browser must never see under a private (non-`public`) key.
177
+
178
+ ## Calling a composable after `await`: `runWithContext`
179
+
180
+ Composables and `navigateTo` must run in Nuxt's context. After an `await`, that
181
+ context can be lost — the classic **"Nuxt instance unavailable"** error. Restore
182
+ it with `nuxtApp.runWithContext`:
183
+
184
+ ```typescript
185
+ const nuxtApp = useNuxtApp();
186
+ const result = await someAsyncWork();
187
+ return nuxtApp.runWithContext(() => navigateTo("/next")); // ✅ context restored post-await
188
+ ```
189
+
190
+ (Capture `useNuxtApp()`/the composable result *before* the await when you can;
191
+ `runWithContext` is the escape hatch for when you can't.)
192
+
164
193
  ## Summary Table
165
194
 
166
195
  | Location | Naming | Vue APIs | Auto-imported | Use Case |
@@ -176,4 +205,5 @@ export function formatCurrency(amount: number) {
176
205
  2. **Don't use Vue APIs in utils** - Keeps them testable and portable
177
206
  3. **Server utils can't use Vue** - Different runtime
178
207
  4. **Auto-import scoping** - `/utils` is client-only, `/server/utils` is server-only
179
- 5. **Composables call order matters** - Call at top of `<script setup>`, not in callbacks
208
+ 5. **Composables call order matters** - Call at top of `<script setup>`, not in callbacks, and **never inside a template expression** - a composable invoked from the template (or a callback) runs outside the request/setup scope and throws "must be called at the top of setup". Call it once in setup, then use the returned helpers/refs in the template.
209
+ 6. **Guard possibly-null data when deriving** - Composables like `useUserSession` expose refs that can be `null` (logged out, still loading). Derive display state through a `computed` with a null guard (`computed(() => user.value ? getInitials(user.value) : '')`), not a bare property access that assumes presence.
@@ -0,0 +1,74 @@
1
+ # Error Handling (client + server)
2
+
3
+ The skill covers throwing `createError` in API handlers (see `auth-patterns.md`).
4
+ This file is the **client/page** surface: triggering the error page, the
5
+ `error.vue` root, recovering, and containing a failure to a subtree.
6
+
7
+ ## `createError` — fatal vs non-fatal
8
+
9
+ `createError` works on both sides. On the **server** it sets the HTTP status. On
10
+ the **client/page**, a `createError` with `fatal: true` replaces the whole page
11
+ with the error page (`error.vue`); without `fatal`, it's a non-fatal error you
12
+ handle locally:
13
+
14
+ ```typescript
15
+ // page setup — show the full-screen error page for a missing resource
16
+ const { data } = await useFetch(`/api/invoices/${id}`);
17
+ if (!data.value) {
18
+ throw createError({ statusCode: 404, statusMessage: "Invoice not found", fatal: true });
19
+ }
20
+ ```
21
+
22
+ `showError(...)` is the imperative equivalent (call it from a handler instead of
23
+ throwing). Pass `data` on the error to carry structured detail to `error.vue`.
24
+
25
+ ## `error.vue` — the app-root error page
26
+
27
+ A single `error.vue` at the project root renders for any fatal error (and SSR
28
+ 500s). It receives the error as a prop and clears it with `clearError`:
29
+
30
+ ```vue
31
+ <script setup lang="ts">
32
+ const props = defineProps<{ error: NuxtError }>();
33
+ // clearError unmounts the error page; redirect clears the errored route
34
+ const handled = () => clearError({ redirect: "/" });
35
+ </script>
36
+
37
+ <template>
38
+ <div>
39
+ <h1>{{ error.statusCode }}</h1>
40
+ <p>{{ error.statusMessage }}</p>
41
+ <button @click="handled">Go home</button>
42
+ </div>
43
+ </template>
44
+ ```
45
+
46
+ Only `clearError` dismisses the error page — re-rendering won't. Gotcha: on the
47
+ error page, **middleware re-runs** but **plugins do not re-run** until you
48
+ `clearError()`, so don't rely on plugin-provided state inside `error.vue`.
49
+
50
+ ## `<NuxtErrorBoundary>` — contain a failure to a subtree
51
+
52
+ When one widget can fail without taking down the page (a flaky third-party embed,
53
+ an optional panel), wrap it so the error is caught locally instead of bubbling to
54
+ `error.vue`:
55
+
56
+ ```vue
57
+ <NuxtErrorBoundary @error="logError">
58
+ <RiskyWidget />
59
+ <template #error="{ error, clearError }">
60
+ <p>Couldn't load this section.</p>
61
+ <button @click="clearError">Retry</button>
62
+ </template>
63
+ </NuxtErrorBoundary>
64
+ ```
65
+
66
+ ## Which to reach for
67
+
68
+ | Situation | Use |
69
+ |---|---|
70
+ | Resource missing / unauthorized in page setup | `throw createError({ …, fatal: true })` |
71
+ | Imperatively show the error page from a handler | `showError(...)` |
72
+ | Render for any fatal error, app-wide | `error.vue` + `clearError` |
73
+ | One section may fail without killing the page | `<NuxtErrorBoundary>` |
74
+ | API handler rejecting a request | `createError` (status code) — see `auth-patterns.md` |
@@ -121,6 +121,24 @@ const handleDelete = async (id: number) => {
121
121
  };
122
122
  ```
123
123
 
124
+ ## Invalidating after a mutation
125
+
126
+ `refresh()` re-pulls a single `useFetch`. When a mutation affects data loaded by
127
+ **other** components, use the global `refreshNuxtData(keys?)` instead of wiring
128
+ cross-component refresh plumbing — it re-runs every matching `useFetch`/
129
+ `useAsyncData` payload:
130
+
131
+ ```typescript
132
+ await $fetch("/api/invoices", { method: "POST", body });
133
+ await refreshNuxtData(["invoices", "invoice-summary"]); // re-pull by explicit key
134
+ // refreshNuxtData() with no args refetches everything (use sparingly)
135
+ ```
136
+
137
+ For this to target precisely, give the fetches explicit keys
138
+ (`useAsyncData("invoices", …)` / `useFetch("/api/invoices", { key: "invoices" })`).
139
+ `clearNuxtData(keys?)` drops cached payload + error state without refetching (e.g.
140
+ reset a wizard's loaded data on cancel).
141
+
124
142
  ## Type Inference
125
143
 
126
144
  Template literals preserve type inference (fixed late 2024):
@@ -0,0 +1,40 @@
1
+ # Nuxt Layers
2
+
3
+ Layers let multiple Nuxt projects share components, composables, utils, server
4
+ routes, and config. Relevant when a base/template repo feeds several apps — e.g. a
5
+ copier-scaffolded house style, or a shared design system across products.
6
+
7
+ ## Extending
8
+
9
+ ```typescript
10
+ // nuxt.config.ts
11
+ export default defineNuxtConfig({
12
+ extends: [
13
+ "../base-layer", // local path
14
+ "@my-org/nuxt-theme", // npm package
15
+ ["github:my-org/layer", { install: true }], // remote
16
+ ],
17
+ });
18
+ ```
19
+
20
+ Auto-imports work **across** layers — a composable in the base layer is available
21
+ unprefixed in the consumer, same as a local one.
22
+
23
+ ## Precedence
24
+
25
+ Project root **overrides** `~~/layers/*` (alphabetical) **overrides** `extends`
26
+ entries (in listed order). So a consumer can shadow any layer file by placing a
27
+ file at the same path. Local `~~/layers/*` directories are auto-registered without
28
+ listing them in `extends`; they also get `#layers/*` aliases.
29
+
30
+ ## When a layer vs a package vs the agent-skills repo
31
+
32
+ | Sharing | Use |
33
+ |---|---|
34
+ | Runtime Nuxt surface (components, composables, server routes, config) | a **layer** (`extends`) |
35
+ | Framework-agnostic JS/TS logic | a plain npm package |
36
+ | Agent guidance / Claude skills | the `@gallopsystems/agent-skills` package |
37
+
38
+ Don't reach for a layer to share a pure function — that's a package. Layers earn
39
+ their weight when you're sharing *Nuxt-shaped* surface (auto-imported components,
40
+ pages, server handlers) and config.
@@ -0,0 +1,41 @@
1
+ # Route Rules
2
+
3
+ `routeRules` in `nuxt.config.ts` declaratively apply caching, headers, redirects,
4
+ proxying, and CORS per URL pattern — no handler code. Reach for them before
5
+ hand-rolling middleware to do what a rule does in one line.
6
+
7
+ ```typescript
8
+ // nuxt.config.ts
9
+ export default defineNuxtConfig({
10
+ routeRules: {
11
+ "/api/**": { cors: true }, // CORS on all API routes
12
+ "/api/realtime/**": { cache: false }, // never cache SSE/streaming
13
+ "/blog/**": { swr: 3600 }, // cache + revalidate hourly
14
+ "/docs/**": { prerender: true }, // render at build time
15
+ "/old-path": { redirect: "/new-path" }, // 301 (redirectCode to change)
16
+ "/external/**": { proxy: "https://upstream.example/**" },// reverse-proxy
17
+ "/admin/**": { headers: { "X-Robots-Tag": "noindex" } },
18
+ },
19
+ });
20
+ ```
21
+
22
+ ## Semantics
23
+
24
+ - **Specificity wins:** a more specific path overrides a general one; `/api/**`
25
+ rules are overridden by `/api/realtime/**`.
26
+ - **`swr: N`** = stale-while-revalidate for N seconds; **`cache: { maxAge: N }`**
27
+ for a plain TTL; **`cache: false`** disables inherited caching (use on
28
+ streaming/per-user routes that a broader rule would otherwise cache).
29
+ - **`isr`** (incremental static regen) is the same idea on supported platforms.
30
+ - Rules apply to both Nitro routes and rendered pages under the path.
31
+
32
+ ## When to use a rule vs a handler
33
+
34
+ | Want | Use |
35
+ |---|---|
36
+ | Cache/headers/redirect/proxy/CORS by path | `routeRules` |
37
+ | Per-key cache with invalidation, SWR on a function | `defineCachedFunction` — [caching.md](./caching.md) |
38
+ | Logic (auth, body inspection, dynamic redirect) | server middleware / handler — [server-runtime.md](./server-runtime.md) |
39
+
40
+ Don't write a middleware that just sets a static header or redirects a fixed path
41
+ — that's a route rule.
@@ -0,0 +1,97 @@
1
+ # Nitro Server Runtime (version, middleware, context, $fetch, ws)
2
+
3
+ Server-side mechanics that aren't endpoints or validation: which Nitro you're on,
4
+ how `server/middleware/` runs, reading the event without threading it, the
5
+ internal-`$fetch` auth trap, and the WebSocket option.
6
+
7
+ ## ⚠️ Version: Nitro follows Nuxt — you're on v2
8
+
9
+ Nitro's version is **set by Nuxt, not chosen by you**. Nuxt 4.x ships
10
+ `@nuxt/nitro-server`, which depends on `nitropack ^2.x` — you have no direct
11
+ `nitropack` dependency to bump. **`nitro.build` now documents Nitro v3**, a
12
+ separate in-progress major Nuxt has not adopted; copying its examples verbatim
13
+ gives you code that doesn't compile here. Ignore the v3 renames until Nuxt ships
14
+ on v3. Map back to v2:
15
+
16
+ | v3 docs (nitro.build) | v2 (this project) |
17
+ |---|---|
18
+ | `defineHandler` | `defineEventHandler` (auto-imported) |
19
+ | `definePlugin` | `defineNitroPlugin` |
20
+ | `import { defineCachedEventHandler } from "nitro/cache"` | auto-imported global |
21
+ | `features: { websocket: true }` | `experimental: { websocket: true }` |
22
+ | `routes/` directory | `server/` directory |
23
+ | `render:response` → `response` hook | `render:response` |
24
+
25
+ When unsure, trust the installed `node_modules/nitropack/dist/runtime/*.d.ts`
26
+ over the website.
27
+
28
+ ## Server middleware (`server/middleware/`)
29
+
30
+ Every request runs **all** files in `server/middleware/` (not route-scoped).
31
+ Three rules that bite:
32
+
33
+ - **Order is filename order** — prefix numerically: `1.logger.ts`, `2.auth.ts`.
34
+ - **Don't return a value.** Returning a body **ends the request** there. Set
35
+ `event.context.*` and return nothing.
36
+ - **Scope by checking `event.path`** yourself — there's no per-path registration.
37
+
38
+ ```typescript
39
+ // server/middleware/2.auth.ts — runs after 1.*, sets context, returns nothing
40
+ export default defineEventHandler((event) => {
41
+ if (!event.path.startsWith("/api/")) return;
42
+ event.context.user = parseUser(event); // ❌ a `return user` here would 200 every request
43
+ });
44
+ ```
45
+
46
+ ## `useEvent()` — read the event without threading it
47
+
48
+ With `experimental.asyncContext: true` (set in `nuxt.config.ts`), `useEvent()`
49
+ returns the current `H3Event` from async context, so deep util/service layers can
50
+ read the session/headers without every caller passing `event` down:
51
+
52
+ ```typescript
53
+ // nuxt.config.ts → nitro: { experimental: { asyncContext: true } }
54
+ import { useEvent } from "nitropack/runtime"; // auto-imported in server/
55
+
56
+ export async function currentUser() {
57
+ const event = useEvent(); // no event parameter needed
58
+ return (await getUserSession(event)).user;
59
+ }
60
+ ```
61
+
62
+ Node-runtime only; it's flagged experimental. Without the flag, `useEvent()`
63
+ throws — keep threading `event` explicitly.
64
+
65
+ ## Internal `$fetch` does NOT forward cookies
66
+
67
+ On the server, `$fetch("/api/…")` to an internal route short-circuits HTTP (direct
68
+ handler call — faster), but it **does not carry the incoming request's
69
+ cookies/headers**. A session-dependent internal call silently sees no user. Pass
70
+ them through:
71
+
72
+ ```typescript
73
+ // inside a handler, calling another internal route that needs the session:
74
+ await $fetch("/api/me", { headers: { cookie: getHeader(event, "cookie") ?? "" } });
75
+ ```
76
+
77
+ ## WebSockets — when SSE isn't enough
78
+
79
+ [sse.md](./sse.md) covers one-way streaming. For bidirectional (client → server),
80
+ Nitro has `defineWebSocketHandler` behind `experimental.websocket: true` (NOT v3's
81
+ `features.websocket`), with built-in pub/sub:
82
+
83
+ ```typescript
84
+ // server/routes/ws.ts
85
+ export default defineWebSocketHandler({
86
+ open(peer) { peer.subscribe("room"); },
87
+ message(peer, msg) { peer.publish("room", msg.text()); }, // publish excludes sender
88
+ close(peer) { /* cleanup */ },
89
+ });
90
+ ```
91
+
92
+ ## Two `useDatabase`s — don't confuse them
93
+
94
+ Nitro ships its own `useDatabase()` (a `db0`-backed SQL layer, gated on
95
+ `experimental.database`). This project **shadows** it with the Kysely
96
+ `useDatabase()` in `server/utils/db.ts` (see [composables-utils.md](./composables-utils.md)).
97
+ Use the Kysely one; don't enable `nitro.database` or import Nitro's.
@@ -113,6 +113,66 @@ With `@vueuse/nuxt`, these are auto-imported:
113
113
  - `useFetch` - use Nuxt's version
114
114
  - `useHead` - use Nuxt's version
115
115
 
116
+ ## `useCookie` / `useState` for SSR-shared state
117
+
118
+ `useLocalStorage` reads only on the client, so state that must be **correct in the
119
+ first server-rendered paint** (a theme, sidebar-collapsed flag, accounting-basis
120
+ toggle) flickers if stored in localStorage. Persist it in a cookie instead — the
121
+ server sees it and renders the right thing immediately:
122
+
123
+ ```typescript
124
+ const basis = useCookie<"accrual" | "cash">("basis", { default: () => "accrual" });
125
+ // readable during SSR; writes sync to the cookie. No onMounted flicker.
126
+ ```
127
+
128
+ For SSR-shared state that needn't persist across reloads, use `useState` (it
129
+ serializes the server value to the client, so both render identically). See
130
+ [state-management.md](./state-management.md) for the full `useState` story
131
+ (including the never-export-a-module-`ref` rule):
132
+
133
+ ```typescript
134
+ const expanded = useState("nav-expanded", () => false);
135
+ ```
136
+
137
+ `useState` is also the fix for **SSR-nondeterministic values** — anything from
138
+ `Math.random()` / `Date.now()` rendered into markup mismatches on hydration unless
139
+ the server's choice is serialized:
140
+
141
+ ```typescript
142
+ const variant = useState("hero-variant", () => Math.floor(Math.random() * 3)); // server picks once, client reuses
143
+ ```
144
+
145
+ ### `useCookie` options worth knowing
146
+
147
+ - **Nested writes need `watch: 'deep'`** — the default shallow watch won't
148
+ persist a mutation to a nested property of a cookie object.
149
+ - **`httpOnly` cookies are unreadable client-side** — reading one in setup yields
150
+ different values on server vs client → hydration mismatch.
151
+ - **`refreshCookie(name)`** re-syncs the ref if the cookie changed out-of-band
152
+ (e.g. set by an API response).
153
+ - 4 KB size limit (see gotcha below).
154
+
155
+ ## SSR-safe URL & request headers
156
+
157
+ `window.location` and request headers don't exist during SSR. Don't guard them
158
+ away — use the universal composables that work on both sides:
159
+
160
+ - **`useRequestURL()`** → a `URL` object available on server *and* client. Use it
161
+ for `origin`/`hostname`/`searchParams` instead of `window.location`:
162
+
163
+ ```typescript
164
+ const url = useRequestURL();
165
+ const origin = url.origin; // works during SSR; window.location would throw
166
+ ```
167
+
168
+ - **`useRequestHeaders(['cookie'])`** → inbound request headers on the server
169
+ (`{}` on the client). The canonical way to **forward auth** when an SSR
170
+ `useFetch` calls an endpoint that needs the session cookie:
171
+
172
+ ```typescript
173
+ const { data } = await useFetch("/api/me", { headers: useRequestHeaders(["cookie"]) });
174
+ ```
175
+
116
176
  ## Hydration Mismatch Prevention
117
177
 
118
178
  **Problem:** Server renders with default, client reads different value = mismatch.
@@ -0,0 +1,68 @@
1
+ # Shared State with `useState`
2
+
3
+ Cross-component shared state in Nuxt is `useState` — NOT a module-scope `ref`.
4
+ This is the single most important state rule in an SSR app, because the wrong
5
+ version is a cross-request data leak, not just a bug.
6
+
7
+ ## Never export a module-scope `ref`
8
+
9
+ A `ref` declared at module scope is created **once per server process** and shared
10
+ by every request the server handles. During SSR that means one user can see
11
+ another user's data. `useState(key, init)` creates state that is **per-request on
12
+ the server** and hydrated to the client:
13
+
14
+ ```typescript
15
+ // ❌ SHARED ACROSS ALL SSR REQUESTS — leaks state between users
16
+ export const user = ref(null);
17
+
18
+ // ✅ keyed, per-request, hydration-safe — wrap it in a composable
19
+ export const useUser = () => useState("user", () => null);
20
+ ```
21
+
22
+ Always expose `useState` through a `use*` composable so the key is defined once
23
+ and can't drift between call sites.
24
+
25
+ ## Rules
26
+
27
+ - **Provide a factory initializer:** `useState("count", () => 0)`. The init runs
28
+ on the server; the value is serialized into the payload and reused on the
29
+ client (no re-init, no mismatch).
30
+ - **State must be JSON-serializable** — no class instances, functions, `Date`
31
+ round-trips, or `Map`/`Set`. It travels through the SSR payload as JSON.
32
+ - **Same key = same state.** Two `useState("user")` calls anywhere in the app
33
+ read/write the same cell. Namespace keys for anything non-global.
34
+ - **Reset with `clearNuxtState(key?)`** — clears one key or all keyed state (e.g.
35
+ on logout).
36
+
37
+ ```typescript
38
+ // composables/useFilters.ts — app-wide filter state, survives navigation
39
+ export const useFilters = () => useState("filters", () => ({ search: "", page: 1 }));
40
+ ```
41
+
42
+ ## `useState` vs the alternatives
43
+
44
+ | Need | Reach for |
45
+ |---|---|
46
+ | Shared reactive state across components, SSR-safe | `useState` |
47
+ | Per-request state that also persists in the browser across reloads | `useCookie` (small, ≤4 KB) — see [ssr-client.md](./ssr-client.md) |
48
+ | Client-only persistence (no SSR) | VueUse `useLocalStorage` — see [ssr-client.md](./ssr-client.md) |
49
+ | Cached server data | `useFetch`/`useAsyncData` (already keyed state) — see [fetch-patterns.md](./fetch-patterns.md) |
50
+
51
+ `useState` is plain shared state; it does NOT fetch or cache. For server data,
52
+ reach for `useAsyncData`/`useFetch` (which are themselves keyed payload state) and
53
+ invalidate with `refreshNuxtData` rather than mirroring fetched data into a
54
+ separate `useState`.
55
+
56
+ ## Run one-time init with `callOnce`
57
+
58
+ To run a side effect **exactly once** across SSR + hydration (seed a store, fire a
59
+ one-time analytics/init call), use `callOnce` — not `onMounted` + a flag, which
60
+ re-fires on every client mount:
61
+
62
+ ```typescript
63
+ await callOnce("init-analytics", () => initAnalytics());
64
+ // mode: "navigation" (3.15+) re-runs once per client-side navigation instead of once ever
65
+ ```
66
+
67
+ `callOnce` returns nothing — it's for effects. For data, use `useAsyncData` (which
68
+ already de-dupes). The call must be unconditional (don't put it behind an `if`).