Vue3 and nuxt3 architecture
Skill hamzabellouch/agent-skills/Advanced Frontend Frameworks/vue3-and-nuxt3-architecture
Production-grade architecture patterns, state management, SSR strategies, performance optimization, and anti-patterns for Vue 3 (Composition API) and Nuxt 3 applications.From its SKILL.md
npx -y skills add hamzabellouch/agent-skills --skill vue3-and-nuxt3-architectureAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
9.6 KB, ~2.3k tokens by cl100k_base, as published. Nobody here has run it
Vue 3 & Nuxt 3 Enterprise Architecture Guide
Core Architectural Principles
1. Composition API & Composables Design
- Single Responsibility: Each composable should manage one specific domain concept or side-effect (e.g.,
useAuth,useInfiniteScroll,useWebNotification). - Explicit Return Types & Reactivity: Prefer returning an explicit object of
ShallowRef,Ref, orReadonly<Ref>to preserve reactivity encapsulation. - Context Injection: Use
provide/injectwith Symbol keys for scope-level dependency injection (e.g., module configuration, UI themes, scoped dynamic state). - Vue 3.5+ Reactive Props Destructuring: Take advantage of native destructuring for
defineProps()without losing reactivity, utilizing default value assignment natively.
2. Nuxt 3 Full-Stack & Rendering Strategies
- Hybrid Rendering & Route Rules: Assign rendering modes strategically in
nuxt.config.ts(ssr: true,swr,static,prerender) based on page freshness and SEO requirements. - Nitro Engine & Server Routes: Enforce strict validation on
server/api/andserver/routes/using Zod or H3 built-in helpers (readValidatedBody,getValidatedQuery). - Payload Extraction & Hydration: Avoid passing non-serializable objects (DOM nodes, class instances, circular references) in
useAsyncDataoruseState.
3. State Management with Pinia & Nuxt useState
- Setup Stores over Option Stores: Standardize on Setup Store syntax (
defineStore('id', () => { ... })) for improved Composition API synergy and TypeScript inference. - SSR State Isolation: Avoid top-level global variables outside of functions in server routes or composables to prevent multi-tenant cross-request data leaks.
- SSR Hydration Boundaries: Mark client-only states appropriately, using
onMountedor<ClientOnly>wrappers when referencing browser APIs likewindow,localStorage, or Web APIs.
Production Code Examples
Example 1: Type-Safe Dynamic Composable with Auto Cleanup
Location: composables/useWebSocketStream.ts
import { ref, onUnmounted, shallowRef, type Ref } from 'vue'
export interface WebSocketStreamOptions<T> {
url: string
reconnectInterval?: number
maxRetries?: number
transform?: (raw: unknown) => T
}
export interface WebSocketStreamReturn<T> {
data: Ref<T | null>
error: Ref<Error | null>
isConnected: Ref<boolean>
send: (payload: unknown) => void
close: () => void
}
export function useWebSocketStream<T = unknown>(
options: WebSocketStreamOptions<T>
): WebSocketStreamReturn<T> {
const { url, reconnectInterval = 3000, maxRetries = 5, transform } = options
const data = shallowRef<T | null>(null) as Ref<T | null>
const error = ref<Error | null>(null)
const isConnected = ref<boolean>(false)
let socket: WebSocket | null = null
let retryCount = 0
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
const connect = () => {
if (import.meta.server) return // Prevent SSR execution
try {
socket = new WebSocket(url)
socket.onopen = () => {
isConnected.value = true
error.value = null
retryCount = 0
}
socket.onmessage = (event: MessageEvent) => {
try {
const parsed = JSON.parse(event.data)
data.value = transform ? transform(parsed) : (parsed as T)
} catch (e) {
error.value = e instanceof Error ? e : new Error('Failed to parse WebSocket message')
}
}
socket.onerror = (event: Event) => {
error.value = new Error(`WebSocket error observed: ${JSON.stringify(event)}`)
}
socket.onclose = () => {
isConnected.value = false
if (retryCount < maxRetries) {
retryCount++
reconnectTimer = setTimeout(connect, reconnectInterval)
}
}
} catch (e) {
error.value = e instanceof Error ? e : new Error('WebSocket connection initialization failed')
}
}
const send = (payload: unknown) => {
if (socket && isConnected.value) {
socket.send(typeof payload === 'string' ? payload : JSON.stringify(payload))
} else {
console.warn('[useWebSocketStream] Attempted to send payload while disconnected.')
}
}
const close = () => {
if (reconnectTimer) clearTimeout(reconnectTimer)
if (socket) {
socket.onclose = null // Prevent reconnect loop
socket.close()
}
isConnected.value = false
}
connect()
// Ensure leak-free disposal when hosting component unmounts
onUnmounted(() => {
close()
})
return {
data,
error,
isConnected,
send,
close
}
}
Example 2: Nuxt 3 Nitro Server Route with Zod Validation & Rate Limiting
Location: server/api/v1/products.post.ts
import { z } from 'zod'
const CreateProductSchema = z.object({
title: z.string().min(3).max(100),
price: z.number().positive(),
category: z.enum(['electronics', 'apparel', 'books']),
sku: z.string().regex(/^[A-Z]{3}-\d{4}$/),
tags: z.array(z.string()).default([])
})
export type CreateProductInput = z.infer<typeof CreateProductSchema>
export default defineEventHandler(async (event) => {
// Validate request body against schema
const result = await readValidatedBody(event, (body) => CreateProductSchema.safeParse(body))
if (!result.success) {
throw createError({
statusCode: 400,
statusMessage: 'Bad Request',
data: result.error.format()
})
}
const productData = result.data
const config = useRuntimeConfig(event)
try {
// Perform downstream API call or DB interaction using runtimeConfig
const response = await $fetch<{ id: string; createdAt: string }>(`${config.apiSecretUrl}/products`, {
method: 'POST',
headers: {
Authorization: `Bearer ${config.apiSecretKey}`
},
body: productData
})
setResponseStatus(event, 201)
return {
success: true,
data: {
id: response.id,
...productData,
createdAt: response.createdAt
}
}
} catch (err: any) {
throw createError({
statusCode: err.statusCode || 500,
statusMessage: err.statusMessage || 'Internal Server Error'
})
}
})
Example 3: SSR-Safe Pinia Setup Store with Async Hydration
Location: stores/useUserSessionStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface UserProfile {
id: string
email: string
roles: string[]
}
export const useUserSessionStore = defineStore('userSession', () => {
const user = ref<UserProfile | null>(null)
const token = ref<string | null>(null)
const isHydrated = ref(false)
const isAuthenticated = computed(() => !!token.value && !!user.value)
const isAdmin = computed(() => user.value?.roles.includes('ADMIN') ?? false)
function setUser(newUser: UserProfile | null) {
user.value = newUser
}
function setToken(newToken: string | null) {
token.value = newToken
}
async function fetchCurrentUser() {
if (!token.value) return
try {
const data = await useRequestFetch()<UserProfile>('/api/v1/auth/me')
user.value = data
} catch (e) {
logout()
}
}
function logout() {
user.value = null
token.value = null
if (import.meta.client) {
const authCookie = useCookie('auth_token')
authCookie.value = null
}
}
return {
user,
token,
isHydrated,
isAuthenticated,
isAdmin,
setUser,
setToken,
fetchCurrentUser,
logout
}
})
Anti-Patterns & Common Pitfalls
❌ Anti-Pattern 1: Destructuring Props without Reactivity Preservation
// BAD: Destructuring props directly breaks reactivity
const { title, count } = defineProps<{ title: string; count: number }>()
// Changing 'title' or 'count' from parent won't trigger re-evaluations here!
// GOOD (Vue 3.5+ Reactive Props Destructure):
const { title, count = 0 } = defineProps<{ title: string; count?: number }>()
// GOOD (Vue <3.5):
const props = defineProps<{ title: string; count: number }>()
const titleRef = toRef(props, 'title')
❌ Anti-Pattern 2: Global State Leakage Across SSR Requests
// BAD: Declaring mutable state outside composable/store factory function scope
const sharedState = ref({ user: null }) // SHARED ACROSS ALL SSR REQUESTS! User A can see User B's data!
export function useBadState() {
return sharedState
}
// GOOD: Use Nuxt's useState or Pinia store
export function useGoodState() {
return useState('unique-user-state-key', () => ({ user: null }))
}
❌ Anti-Pattern 3: Calling Composables Inside Event Handlers or Async Tasks
// BAD: Calling composables after await or inside nested functions breaks Vue lifecycle tracking
async function handleClick() {
await fetchData()
const route = useRoute() // ERROR: Current instance lost after await!
}
// GOOD: Instantiate composables at top level of <script setup>
const route = useRoute()
async function handleClick() {
await fetchData()
console.log(route.path)
}
❌ Anti-Pattern 4: Direct Mutation of Props
// BAD: Mutating props directly
const props = defineProps<{ modelValue: string }>()
function updateText(val: string) {
props.modelValue = val // Vue warning! Direct prop mutation is forbidden.
}
// GOOD: Emit update event or use defineModel() (Vue 3.4+)
const modelValue = defineModel<string>()
function updateText(val: string) {
modelValue.value = val
}
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.