15 Prompts for Vue.js and Nuxt: Components, Composables, Routing, and Pinia Stores

Introduction

Building modern Vue.js and Nuxt applications requires more than just understanding the framework—it demands structured thinking about state management, reusable logic, and efficient routing. Whether you are a junior developer struggling with component decomposition or a senior architect optimizing large-scale apps, the right prompts can act as a scaffold for your code. In this article, I share 15 battle-tested prompts for generating Vue components, Nuxt composables, routing patterns, and Pinia stores. Each prompt is based on real-world scenarios from projects like a dashbord for an e-commerce platform and a multi-tenant SaaS app. The prompts are designed to be copy-pasted into your AI assistant or used as mental templates.

1. Generate a Reusable Vue Component with Props and Slots

Task: Create a generic card component that accepts a title (string), an optional subtitle (string), and a default slot for body content. The component should emit a 'click' event when the card is clicked.

Prompt:

Generate a Vue 3 single-file component (SFC) named BaseCard.vue using Composition API with <script setup>. It must accept props: title (String, required), subtitle (String, default: null). Provide a default slot for body content. Emit a 'click' event when the card header is clicked. Include scoped styles with a clean border, padding, and hover effect. Write the full code.

Example Result:

<template>
  <div class="base-card" @click="$emit('click')">
    <div class="card-header">
      <h3>{{ title }}</h3>
      <p v-if="subtitle" class="subtitle">{{ subtitle }}</p>
    </div>
    <div class="card-body">
      <slot />
    </div>
  </div>
</template>

<script setup>
defineProps({
  title: { type: String, required: true },
  subtitle: { type: String, default: null }
})
defineEmits(['click'])
</script>

<style scoped>
.base-card { border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; cursor: pointer; transition: box-shadow 0.2s; }
.base-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.card-header { margin-bottom: 12px; }
.subtitle { color: #666; font-size: 0.9em; }
</style>

2. Create a Nuxt Composable for API Data Fetching

Task: Build a composable that wraps useFetch with error handling, loading state, and automatic refetch on parameter change.

Prompt:

Write a Nuxt 3 composable called useApiFetch that accepts a URL (string) and an options object (with params, headers). It should return reactive data, error (ref), and pending (ref). Use the built-in useFetch under the hood but add a custom error handler that logs to console. Include TypeScript generics for the response type. Example usage: const { data, error } = await useApiFetch<User>('/api/users', { params: { page: 1 } }).

Example Result:

// composables/useApiFetch.ts
export function useApiFetch<T>(url: string, options?: { params?: Record<string, any>; headers?: HeadersInit }) {
  const { data, error, pending, refresh } = useFetch<T>(url, {
    ...options,
    onResponseError({ response }) {
      console.error('[API Error]', response.status, response._data)
    }
  })
  return { data, error, pending, refresh }
}

3. Design a Pinia Store for User Authentication

Task: Create a Pinia store that manages user state, login, logout, and token persistence with localStorage.

Prompt:

Generate a Pinia store (Options API style) named useAuthStore with state: user (null

| object), token (string | null), loading (boolean). Actions: login(email, password) that calls a mock API, stores token in localStorage, and sets user. logout() clears state and localStorage. Getter: isAuthenticated returns boolean. Include a reset action. Use TypeScript.

Example Result:

// stores/auth.ts
export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null as User | null,
    token: localStorage.getItem('token') || null,
    loading: false
  }),
  getters: {
    isAuthenticated: (state) => !!state.token
  },
  actions: {
    async login(email: string, password: string) {
      this.loading = true
      try {
        const res = await $fetch<{ user: User; token: string }>('/api/login', { method: 'POST', body: { email, password } })
        this.user = res.user
        this.token = res.token
        localStorage.setItem('token', res.token)
      } finally {
        this.loading = false
      }
    },
    logout() {
      this.user = null
      this.token = null
      localStorage.removeItem('token')
    },
    reset() {
      this.$reset()
      localStorage.removeItem('token')
    }
  }
})

4. Implement Dynamic Routing with Nuxt Middleware

Prompt:

Generate a Nuxt 3 middleware file (middleware/auth.global.ts) that checks if the user is authenticated using the useAuthStore. If not authenticated and the route is not /login or /register, redirect to /login. Use navigateTo from nuxt/app. Add a console log for debugging.

Example Result:

export default defineNuxtRouteMiddleware((to) => {
  const auth = useAuthStore()
  if (!auth.isAuthenticated && to.path !== '/login' && to.path !== '/register') {
    console.log('Redirecting to login from', to.path)
    return navigateTo('/login')
  }
})

5. Create a Composable for Form Validation

Task: Build a composable that validates form fields based on a rules object, returns errors object, and a validate function.

Prompt:

Write a composable useFormValidation that accepts a fields object (key-value pairs) and a rules object (same keys, each value is an array of validation functions). Each validation function receives the value and returns error string or null. The composable returns: errors (reactive object with same keys, values are string or null), validate() method that runs all rules and returns boolean isValid. Example: const { errors, validate } = useFormValidation({ email: 'test@example.com' }, { email: [required, isEmail] })

Example Result:

export function useFormValidation(fields: Record<string, any>, rules: Record<string, Array<(v: any) => string | null>>) {
  const errors = reactive<Record<string, string | null>>({})
  function validate(): boolean {
    let isValid = true
    for (const key of Object.keys(fields)) {
      const value = fields[key]
      const fieldRules = rules[key] || []
      for (const rule of fieldRules) {
        const error = rule(value)
        if (error) {
          errors[key] = error
          isValid = false
          break
        } else {
          errors[key] = null
        }
      }
    }
    return isValid
  }
  return { errors, validate }
}

6. Optimize List Rendering with v-memo

Prompt:

Explain how to use v-memo in Vue 3 to optimize rendering of a long list of items where only a few properties change. Provide a code example with a list of comments where only the 'likes' count updates frequently. Show the template with v-memo="[comment.likes]" and discuss performance benefits.

Example Result:

<template>
  <div v-for="comment in comments" :key="comment.id" v-memo="[comment.likes]">
    <p>{{ comment.text }}</p>
    <span>Likes: {{ comment.likes }}</span>
  </div>
</template>

This tells Vue to skip re-rendering the element unless comment.likes changes, reducing DOM operations when other data (like a timestamp) updates.

7. Create a Nuxt Plugin for Global Error Handling

Prompt:

Write a Nuxt 3 plugin (plugins/error-handler.ts) that adds a global Vue error handler using app.config.errorHandler. The handler should log errors to console and show a toast notification using a hypothetical useToast composable. Use provide/inject to make a custom error reporting function available across the app.

Example Result:

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.config.errorHandler = (err, instance, info) => {
    console.error('Global error:', err, info)
    const { showToast } = useToast()
    showToast('An unexpected error occurred', 'error')
  }
  nuxtApp.provide('reportError', (msg: string) => {
    console.warn('Reported:', msg)
  })
})

8. Build a Reusable Modal Component with Teleport

Prompt:

Create a Vue 3 Modal component that uses Teleport to render its content at the document body. Props: open (boolean), title (string). Emits: close. Include a backdrop overlay, close button, and a default slot for content. Add transition for fade-in/out. Use <script setup>.

Example Result:

<template>
  <Teleport to="body">
    <Transition name="fade">
      <div v-if="open" class="modal-overlay" @click.self="$emit('close')">
        <div class="modal-content">
          <header>
            <h2>{{ title }}</h2>
            <button @click="$emit('close')">X</button>
          </header>
          <slot />
        </div>
      </div>
    </Transition>
  </Teleport>
</template>
<script setup>
defineProps({ open: Boolean, title: String })
defineEmits(['close'])
</script>
<style scoped>
.modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; }
.modal-content { background: white; padding: 20px; border-radius: 8px; min-width: 300px; }
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>

9. Configure Nuxt i18n with Dynamic Locale Switching

Prompt:

Write a Nuxt 3 configuration (nuxt.config.ts) for the @nuxtjs/i18n module with two locales: en and fr. Set defaultLocale to 'en'. Include a strategy: 'prefix_except_default'. Show how to use $t in a component template and how to switch locale programmatically using useI18n from the module.

Example Result:

export default defineNuxtConfig({
  modules: ['@nuxtjs/i18n'],
  i18n: {
    locales: [
      { code: 'en', iso: 'en-US', file: 'en.json' },
      { code: 'fr', iso: 'fr-FR', file: 'fr.json' }
    ],
    defaultLocale: 'en',
    strategy: 'prefix_except_default'
  }
})

In a component: <p>{{ $t('welcome') }}</p>. To switch: const { setLocale } = useI18n(); setLocale('fr').

10. Design a Pinia Store for a Shopping Cart

Prompt:

Generate a Pinia store (Setup Store style) named useCartStore with state: items (array of { id, name, price, quantity }). Actions: addItem(item) increments quantity if exists, removeItem(id), clearCart(). Getters: totalItems (sum of quantities), totalPrice (sum of price*quantity). Persist cart to localStorage using a watcher.

Example Result:

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>(JSON.parse(localStorage.getItem('cart') || '[]'))
  watch(items, (val) => localStorage.setItem('cart', JSON.stringify(val)), { deep: true })
  function addItem(item: CartItem) {
    const existing = items.value.find(i => i.id === item.id)
    if (existing) existing.quantity++
    else items.value.push({ ...item, quantity: 1 })
  }
  function removeItem(id: string) {
    items.value = items.value.filter(i => i.id !== id)
  }
  function clearCart() { items.value = [] }
  const totalItems = computed(() => items.value.reduce((sum, i) => sum + i.quantity, 0))
  const totalPrice = computed(() => items.value.reduce((sum, i) => sum + i.price * i.quantity, 0))
  return { items, addItem, removeItem, clearCart, totalItems, totalPrice }
})

11. Create a Custom Directive for Click Outside

Prompt:

Write a Vue 3 custom directive v-click-outside that calls a function when the user clicks outside the element. Register it globally in a Nuxt plugin (plugins/click-outside.ts). Example usage: <div v-click-outside="closeDropdown">. Include cleanup in beforeUnmount.

Example Result:

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.directive('click-outside', {
    mounted(el, binding) {
      el.__clickOutsideHandler = (event: MouseEvent) => {
        if (!el.contains(event.target)) binding.value()
      }
      document.addEventListener('click', el.__clickOutsideHandler)
    },
    beforeUnmount(el) {
      document.removeEventListener('click', el.__clickOutsideHandler)
    }
  })
})

12. Generate a Nuxt Server Route for API Proxy

Prompt:

Create a Nuxt 3 server route (server/api/proxy/[...slug].ts) that proxies requests to an external API (https://api.example.com). Pass through query parameters and headers. Return the response as JSON. Handle errors with 500 status.

Example Result:

export default defineEventHandler(async (event) => {
  const slug = getRouterParam(event, 'slug')
  const query = getQuery(event)
  const url = `https://api.example.com/${slug}?${new URLSearchParams(query as any)}`
  try {
    const data = await $fetch(url, { headers: { 'Authorization': 'Bearer secret' } })
    return data
  } catch (err) {
    throw createError({ statusCode: 500, statusMessage: 'Proxy failed' })
  }
})

Conclusion

These 15 prompts cover the most common patterns in Vue.js and Nuxt development—from basic components to advanced server routes. By internalizing these templates, you can reduce boilerplate, enforce consistency across your team, and accelerate development. Whether you use them directly with an AI assistant or adapt them to your own coding style, they serve as a solid foundation. Next time you start a new feature, try using one of these prompts as a starting point. Happy coding!

← All posts

Comments