| name | vue3-and-nuxt3-architecture |
| description | Production-grade architecture patterns, state management, SSR strategies, performance optimization, and anti-patterns for Vue 3 (Composition API) and Nuxt 3 applications. |
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, or Readonly<Ref> to preserve reactivity encapsulation.
- Context Injection: Use
provide/inject with 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/ and server/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
useAsyncData or useState.
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
onMounted or <ClientOnly> wrappers when referencing browser APIs like window, 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) <T | >
error = ref< | >()
isConnected = ref<>()
: | =
retryCount =
: < > | =
= () => {
(..)
{
socket = (url)
socket. = {
isConnected. =
error. =
retryCount =
}
socket. = {
{
parsed = .(event.)
data. = transform ? (parsed) : (parsed T)
} (e) {
error. = e ? e : ()
}
}
socket. = {
error. = ()
}
socket. = {
isConnected. =
(retryCount < maxRetries) {
retryCount++
reconnectTimer = (connect, reconnectInterval)
}
}
} (e) {
error. = e ? e : ()
}
}
= () => {
(socket && isConnected.) {
socket.( payload === ? payload : .(payload))
} {
.()
}
}
= () => {
(reconnectTimer) (reconnectTimer)
(socket) {
socket. =
socket.()
}
isConnected. =
}
()
( {
()
})
{
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) => {
const result = await readValidatedBody(event, (body) => CreateProductSchema.safeParse(body))
if (!result.success) {
throw createError({
statusCode: ,
: ,
: result..()
})
}
productData = result.
config = (event)
{
response = $fetch<{ : ; : }>(, {
: ,
: {
:
},
: productData
})
(event, )
{
: ,
: {
: response.,
...productData,
: response.
}
}
} (: ) {
({
: err. || ,
: err. ||
})
}
})
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() {
token. = newToken
}
() {
(!token.)
{
data = ()<>()
user. = data
} (e) {
()
}
}
() {
user. =
token. =
(..) {
authCookie = ()
authCookie. =
}
}
{
user,
token,
isHydrated,
isAuthenticated,
isAdmin,
setUser,
setToken,
fetchCurrentUser,
logout
}
})
Anti-Patterns & Common Pitfalls
❌ Anti-Pattern 1: Destructuring Props without Reactivity Preservation
const { title, count } = defineProps<{ title: string; count: number }>()
const { title, count = 0 } = defineProps<{ title: string; count?: number }>()
const props = defineProps<{ title: string; count: number }>()
const titleRef = toRef(props, 'title')
❌ Anti-Pattern 2: Global State Leakage Across SSR Requests
const sharedState = ref({ user: null })
export function useBadState() {
return sharedState
}
export function useGoodState() {
return useState('unique-user-state-key', () => ({ user: null }))
}
❌ Anti-Pattern 3: Calling Composables Inside Event Handlers or Async Tasks
async function handleClick() {
await fetchData()
const route = useRoute()
}
const route = useRoute()
async function handleClick() {
await fetchData()
console.log(route.path)
}
❌ Anti-Pattern 4: Direct Mutation of Props
const props = defineProps<{ modelValue: string }>()
function updateText(val: string) {
props.modelValue = val
}
const modelValue = defineModel<string>()
function updateText(val: string) {
modelValue.value = val
}