// Config Record
>.cursor/rules — Vue 3 + TypeScript (Nuxt 3)
Cursor MDC project rules for Nuxt 3 and Vue 3 with Composition API, Nitro routes, and typed data-fetching conventions.
author:
dotmd Team
license:CC0
published:Feb 23, 2026
// Installation
>Add this file to your project repository:
- Cursor--path=
.cursor/rules/
// File Content
.cursor/rules (MDC)
1---2description: Vue 3 + TypeScript project using Nuxt 33globs:4alwaysApply: true5---67# Nuxt 3 + Vue 3 + TypeScript89## Component Architecture1011Every Vue component uses `<script setup lang="ts">`. No Options API. No `defineComponent()` unless wrapping a library.1213```vue14<script setup lang="ts">15interface Props {16 title: string17 count?: number18}1920const props = withDefaults(defineProps<Props>(), {21 count: 0,22})2324const emit = defineEmits<{25 update: [value: string]26 close: []27}>()28</script>2930<template>31 <div>32 <h1>{{ title }}</h1>33 </div>34</template>35```3637Type props with TypeScript interfaces, not runtime validation. Use `withDefaults` for default values. Type emits with the tuple syntax.3839Use `defineModel` for two-way binding:4041```vue42<script setup lang="ts">43const modelValue = defineModel<string>({ required: true })44const count = defineModel<number>('count', { default: 0 })45</script>46```4748## Auto-Imports4950Nuxt auto-imports Vue APIs, composables, and utils. Never manually import these:5152- **Vue:** `ref`, `computed`, `watch`, `watchEffect`, `onMounted`, `nextTick`, `toRef`, `toRefs`53- **Nuxt:** `useFetch`, `useAsyncData`, `useState`, `useRuntimeConfig`, `useRoute`, `useRouter`, `navigateTo`, `useHead`, `useSeoMeta`, `definePageMeta`, `useNuxtApp`, `useCookie`, `useRequestHeaders`54- **Your composables:** anything exported from `composables/` is auto-imported55- **Your utils:** anything exported from `utils/` is auto-imported5657If you see `import { ref } from 'vue'` or `import { useFetch } from '#imports'` — remove it. Nuxt handles this.5859## File Conventions6061Follow Nuxt's directory structure strictly:6263| Directory | Purpose |64|---|---|65| `pages/` | File-based routing. `pages/users/[id].vue` → `/users/:id` |66| `components/` | Auto-imported components. `components/ui/Button.vue` → `<UiButton>` |67| `composables/` | Auto-imported composables. Must export `use*` functions |68| `utils/` | Auto-imported utility functions |69| `layouts/` | Page layouts. `default.vue` applies unless overridden |70| `middleware/` | Route middleware. Named (`auth.ts`) or inline |71| `server/api/` | API routes handled by Nitro. `server/api/users.get.ts` → `GET /api/users` |72| `server/routes/` | Non-API server routes |73| `server/middleware/` | Server-side middleware (runs on every request) |74| `server/utils/` | Auto-imported server utilities |75| `plugins/` | Nuxt plugins. `.server.ts` / `.client.ts` suffixes for environment-specific |76| `public/` | Static assets served at root |77| `assets/` | Build-processed assets (CSS, images for bundler) |7879## Data Fetching8081Use `useFetch` for simple API calls and `useAsyncData` when you need more control. Both handle SSR hydration automatically.8283```vue84<script setup lang="ts">85// Simple fetch — uses the route path as the cache key86const { data: users, status } = await useFetch('/api/users')8788// With params and transform89const { data: user } = await useFetch(`/api/users/${id}`, {90 query: { include: 'posts' },91 transform: (response) => response.data,92})9394// useAsyncData for custom logic95const { data: dashboard } = await useAsyncData('dashboard', () => {96 return Promise.all([97 $fetch('/api/stats'),98 $fetch('/api/activity'),99 ])100})101</script>102```103104Rules:105- Use `useFetch` over raw `$fetch` in components — it deduplicates requests and handles SSR106- Use `$fetch` inside server routes, event handlers, and non-component code107- Use `status` (`idle`, `pending`, `success`, `error`) instead of separate boolean flags108- Set a `key` option when the URL is dynamic and you need to refetch: `useFetch('/api/users', { key: \`user-${id}\` })`109- Use `lazy: true` for non-critical data that shouldn't block navigation110111## Server Routes (Nitro)112113Server routes are the backend. File name encodes the HTTP method.114115```ts116// server/api/users.get.ts117export default defineEventHandler(async (event) => {118 const query = getQuery(event)119 const users = await db.user.findMany({ take: Number(query.limit) || 20 })120 return users121})122123// server/api/users.post.ts124export default defineEventHandler(async (event) => {125 const body = await readValidatedBody(event, userSchema.parse)126 const user = await db.user.create({ data: body })127 return user128})129130// server/api/users/[id].get.ts131export default defineEventHandler(async (event) => {132 const id = getRouterParam(event, 'id')133 const user = await db.user.findUnique({ where: { id } })134 if (!user) {135 throw createError({ statusCode: 404, statusMessage: 'User not found' })136 }137 return user138})139```140141Validate request bodies with `readValidatedBody` and a Zod schema. Validate query params with `getValidatedQuery`. Throw errors with `createError`.142143## State Management144145Use `useState` for simple shared state that needs SSR support:146147```ts148// composables/useCounter.ts149export function useCounter() {150 const count = useState<number>('counter', () => 0)151 const increment = () => count.value++152 return { count, increment }153}154```155156Use Pinia with setup stores for complex state:157158```ts159// stores/auth.ts160export const useAuthStore = defineStore('auth', () => {161 const user = ref<User | null>(null)162 const isLoggedIn = computed(() => !!user.value)163164 async function login(credentials: LoginCredentials) {165 user.value = await $fetch('/api/auth/login', {166 method: 'POST',167 body: credentials,168 })169 }170171 function logout() {172 user.value = null173 navigateTo('/login')174 }175176 return { user, isLoggedIn, login, logout }177})178```179180Install with `@pinia/nuxt` module. Stores are auto-imported from `stores/` when using the module.181182Use `useState` for SSR-safe reactive state that's simple. Use Pinia when you need getters, actions, devtools, or the state is complex. Don't use plain `ref()` at module scope for shared state — it leaks across requests on the server.183184## Middleware185186Route middleware for auth and navigation guards:187188```ts189// middleware/auth.ts190export default defineNuxtRouteMiddleware((to, from) => {191 const { isLoggedIn } = useAuthStore()192 if (!isLoggedIn) {193 return navigateTo('/login')194 }195})196```197198Apply in pages with `definePageMeta`:199200```vue201<script setup lang="ts">202definePageMeta({203 middleware: 'auth',204 layout: 'dashboard',205})206</script>207```208209For global middleware, name the file with a `.global.ts` suffix.210211## TypeScript212213Use strict TypeScript. Define types for all API responses, props, and emits.214215```ts216// types/index.ts217export interface User {218 id: string219 email: string220 name: string221 createdAt: string222}223224export interface ApiResponse<T> {225 data: T226 meta?: { total: number; page: number }227}228```229230The `types/` directory is not auto-imported — use explicit imports for type definitions. This is intentional; types are contracts and should be traceable.231232For Nitro event typing, use `EventHandlerRequest`:233234```ts235// server/api/users.post.ts236export default defineEventHandler<{ body: CreateUserInput }>(async (event) => {237 const body = await readBody(event)238 // body is typed as CreateUserInput239})240```241242## Runtime Config243244Use `runtimeConfig` in `nuxt.config.ts` for environment variables:245246```ts247// nuxt.config.ts248export default defineNuxtConfig({249 runtimeConfig: {250 dbUrl: '', // Server-only, set via NUXT_DB_URL251 apiSecret: '', // Server-only, set via NUXT_API_SECRET252 public: {253 apiBase: '', // Client + server, set via NUXT_PUBLIC_API_BASE254 },255 },256})257```258259Access in code:260261```ts262// In components/composables263const config = useRuntimeConfig()264config.public.apiBase // ✅ available everywhere265266// In server routes267const config = useRuntimeConfig()268config.dbUrl // ✅ server-only values available here269```270271Never hardcode secrets. Never access private runtime config on the client.272273## Styling274275Use one of:276- **UnoCSS** (`@unocss/nuxt`) — atomic CSS, Tailwind-compatible presets277- **Tailwind CSS** (`@nuxtjs/tailwind`) — utility-first CSS278- **Scoped styles** — `<style scoped>` for component-specific CSS279280Prefer utility classes in templates. Use `<style scoped>` when you need complex selectors or animations. Never use `<style>` without `scoped` in components — it leaks globally.281282For design systems, use **Nuxt UI** (`@nuxt/ui`) which provides accessible components built on Reka UI with Tailwind.283284## SEO and Head Management285286Use `useSeoMeta` for type-safe SEO:287288```vue289<script setup lang="ts">290useSeoMeta({291 title: 'My Page',292 description: 'Page description for search engines',293 ogTitle: 'My Page',294 ogDescription: 'Page description for social sharing',295 ogImage: '/og-image.png',296})297</script>298```299300Use `useHead` for non-SEO head tags (scripts, links, structured data).301302## Error Handling303304Use `<NuxtErrorBoundary>` for component-level error recovery. Create `error.vue` at the project root for full-page errors.305306In server routes, always use `createError` — never throw plain Error objects:307308```ts309throw createError({310 statusCode: 422,311 statusMessage: 'Validation failed',312 data: { errors: validationErrors },313})314```315316In components, handle fetch errors through the `error` return from `useFetch`:317318```vue319<script setup lang="ts">320const { data, error } = await useFetch('/api/resource')321</script>322323<template>324 <div v-if="error">Something went wrong: {{ error.message }}</div>325 <div v-else-if="data">{{ data }}</div>326</template>327```328329## Testing330331Use **Vitest** for unit tests and **@nuxt/test-utils** for Nuxt-aware testing:332333```ts334// tests/components/UserCard.nuxt.spec.ts335import { mountSuspended } from '@nuxt/test-utils/runtime'336import UserCard from '~/components/UserCard.vue'337338describe('UserCard', () => {339 it('renders user name', async () => {340 const wrapper = await mountSuspended(UserCard, {341 props: { user: { id: '1', name: 'Alice', email: 'alice@example.com' } },342 })343 expect(wrapper.text()).toContain('Alice')344 })345})346```347348Use `mountSuspended` instead of `mount` from `@vue/test-utils` — it handles Nuxt's async setup and auto-imports.349350For server route tests, use `$fetch` from `@nuxt/test-utils/e2e` or test handlers directly.351352## Key Conventions3533541. One component per file. Name matches usage: `UserCard.vue` → `<UserCard>`3552. Composables start with `use`: `useAuth`, `useFilters`, `useFormValidation`3563. Prefer `<template>` logic (`v-if`, `v-for`) over render functions3574. Use `<Suspense>` awareness — top-level `await` in `<script setup>` is fine, Nuxt handles it3585. Use `definePageMeta` for page-level config, not component options3596. Keep server routes thin — extract business logic into `server/utils/`3607. Use `$fetch` in server routes to call other server routes (stays server-side, no HTTP overhead)3618. Prefer `navigateTo()` over `router.push()` — it works in both client and server contexts362