---
name: nuxt-3-composables
description: "Nuxt 3 modules, composables, server routes, Nitro deploys — the Vue full-stack workflow. Writes idiomatic Nuxt 3: useFetch / useAsyncData, Pinia stores, server API routes, and Nitro deploy presets for Vercel / Cloudflare."
license: MIT
---

# Nuxt 3 Composables Pro

Nuxt 3 Composables Pro helps you write idiomatic full-stack Nuxt 3 — the data-fetching composables, server routes, Pinia stores, and Nitro deploy config that a real Vue app needs. It's aimed at developers who know Vue 3 but want Nuxt's conventions applied correctly, especially around SSR hydration, server/client boundaries, and per-target deployment.

## When to use it

- You're scaffolding a page that fetches data and keep getting hydration mismatches or double requests.
- You need a `server/api/` route plus a typed composable to call it, without hand-rolling `$fetch` types.
- You're moving a store to Pinia and want the `defineStore` setup-syntax version, not the options API.
- You're switching deploy targets (Vercel → Cloudflare) and need the right `nitro.preset` and edge caveats.

## What it does

The skill writes against the actual Nuxt 3 / Nitro APIs:

- **Data fetching:** `useFetch`, `useAsyncData`, and `useLazyFetch`, picking the right one based on whether you need blocking SSR or deferred client load. It sets `key`, `server`, `transform`, and `pick` correctly and uses `$fetch` for event-handler calls (mutations) rather than the composables.
- **Server routes:** handlers in `server/api/` using `defineEventHandler`, `getQuery`, `readBody`, and `createError`. Route params via `[id].get.ts` naming.
- **State:** Pinia stores with `defineStore('name', () => { ... })` setup syntax, plus `useState` for lightweight SSR-safe shared state.
- **Deploy:** `nitro.preset` in `nuxt.config.ts` for `vercel`, `vercel-edge`, `cloudflare-pages`, and `cloudflare-module`, including `routeRules` for ISR/`prerender`/`swr`.

## Example

```ts
// server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  const post = await db.posts.find(id)
  if (!post) throw createError({ statusCode: 404, statusMessage: 'Not found' })
  return post
})
```

```vue
<script setup lang="ts">
const route = useRoute()
const { data: post, pending, error } = await useFetch(
  () => `/api/posts/${route.params.id}`,
  { key: `post-${route.params.id}` }
)
</script>
```

## Tips & gotchas

- Always set an explicit `key` on `useFetch`/`useAsyncData` when the URL is dynamic — auto-generated keys can collide and serve stale payloads.
- Use `$fetch` inside `onClick`/submit handlers; `useFetch` runs twice (server + client) and isn't meant for mutations.
- `useState` survives SSR transfer; a plain `ref` in a composable is shared across requests on the server and leaks state between users.
- `cloudflare-module` (Workers) and `cloudflare-pages` are different presets — Node built-ins differ, so check before assuming `node:fs` works.

## FAQ

**`useFetch` vs `useAsyncData`?** Use `useFetch` for a straight URL call. Reach for `useAsyncData` when you wrap custom logic or multiple calls and need full control over the resolver function.

**Why do I get a hydration mismatch?** Usually rendering `Date.now()`, random values, or `import.meta.client`-only data during SSR. The skill flags these and guards them with `<ClientOnly>` or `process.client`.

**Does it support Nuxt 2?** No — it targets Nuxt 3 / Nitro and its composable and server-route conventions.
