14 Prompts for Vue.js and Nuxt: Components, Composables, and Routing
Building modern frontend applications with Vue.js and Nuxt often involves repetitive patterns: creating components, managing state with composables, setting up routes, or handling API calls. Instead of reinventing the wheel each time, you can use battle-tested prompts to generate clean, production-ready code. This collection covers 14 practical prompts—from basic component templates to advanced Pinia stores—that I use daily in my workflow.
Why Use Prompts for Vue.js and Nuxt?
Prompts act as reusable code scaffolds. They reduce boilerplate, enforce best practices (like TypeScript strict mode or the Composition API), and speed up prototyping. According to the official Vue documentation, the Composition API is recommended for new projects, and Nuxt 3 defaults to it. Using prompts ensures consistency across your codebase.
1. Prompt: Create a Typed Vue Component with Composition API
Generate a single-file component (SFC) with TypeScript, using <script setup lang="ts"> and scoped styles.
Example prompt:
"Create a reusable Vue 3 component named UserCard that accepts name, email, and avatarUrl props. Use TypeScript interfaces for props. Include a slot for custom actions. Style with scoped CSS and a hover effect."
Generated code snippet:
<template>
<div class="user-card">
<img :src="avatarUrl" :alt="name" class="avatar" />
<div class="info">
<h3>{{ name }}</h3>
<p>{{ email }}</p>
</div>
<div class="actions">
<slot name="actions" />
</div>
</div>
</template>
<script setup lang="ts">
interface UserCardProps {
name: string;
email: string;
avatarUrl: string;
}
defineProps<UserCardProps>();
</script>
<style scoped>
.user-card {
display: flex;
align-items: center;
padding: 1rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
transition: box-shadow 0.2s;
}
.user-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 1rem;
}
.info {
flex: 1;
}
.actions {
margin-left: auto;
}
</style>
Why it works: The prompt enforces TypeScript interfaces for props, uses the Composition API (recommended by Vue core team), and includes a slot for flexibility.
2. Prompt: Build a Composable for API Calls with Error Handling
Create a composable that wraps fetch with loading, error, and data states, plus automatic cancellation on unmount.
Example prompt:
"Write a Vue 3 composable useApi that takes a URL string, returns data, loading, and error refs. Use AbortController for cleanup in onUnmounted. Include a refresh function to re-fetch."
Generated code snippet:
import { ref, onUnmounted } from 'vue';
export function useApi<T>(url: string) {
const data = ref<T | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
let controller: AbortController | null = null;
const fetchData = async () => {
controller?.abort();
controller = new AbortController();
loading.value = true;
error.value = null;
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
data.value = await response.json() as T;
} catch (err: any) {
if (err.name !== 'AbortError') {
error.value = err.message;
}
} finally {
loading.value = false;
}
};
fetchData();
onUnmounted(() => controller?.abort());
return { data, loading, error, refresh: fetchData };
}
Why it works: This pattern is used in many production apps. The AbortController prevents memory leaks from stale requests.
3. Prompt: Nuxt 3 Page with Dynamic Route and SEO Meta
Generate a Nuxt 3 page for a blog post with dynamic slug, using useHead for SEO.
Example prompt:
"Create a Nuxt 3 page component pages/blog/[slug].vue. Fetch post data using useFetch with the slug from route params. Set page title and meta description dynamically using useHead. Handle loading and error states."
Generated code snippet:
<template>
<div>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<article v-else>
<h1>{{ post.title }}</h1>
<p>{{ post.body }}</p>
</article>
</div>
</template>
<script setup lang="ts">
const route = useRoute();
const slug = route.params.slug as string;
const { data: post, loading, error } = useFetch(`/api/posts/${slug}`);
useHead({
title: post.value?.title || 'Post',
meta: [
{ name: 'description', content: post.value?.excerpt || 'Blog post' },
],
});
</script>
Why it works: Nuxt's useFetch automatically deduplicates requests and handles SSR. useHead updates the document head reactively.
4. Prompt: Pinia Store with TypeScript and Actions
Create a Pinia store for a todo list with typed state, getters, and async actions.
Example prompt:
"Write a Pinia store useTodoStore using the Composition API style. Include a todos array with id, title, and completed fields. Add a getter for incomplete todos and an action to toggle completion. Use defineStore with TypeScript."
Generated code snippet:
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
interface Todo {
id: number;
title: string;
completed: boolean;
}
export const useTodoStore = defineStore('todo', () => {
const todos = ref<Todo[]>([]);
const incompleteTodos = computed(() => todos.value.filter(t => !t.completed));
function addTodo(title: string) {
todos.value.push({ id: Date.now(), title, completed: false });
}
async function toggleTodo(id: number) {
const todo = todos.value.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
// Example: sync with backend
// await fetch(`/api/todos/${id}`, { method: 'PATCH', body: JSON.stringify({ completed: todo.completed }) });
}
}
return { todos, incompleteTodos, addTodo, toggleTodo };
});
Why it works: Pinia's Composition API style offers better TypeScript inference and flexibility compared to the Options API.
5. Prompt: Vue Router Navigation Guard for Authentication
Set up a route guard that redirects unauthenticated users to login.
Example prompt:
"Write a Vue Router navigation guard beforeEach that checks a Pinia auth store. If user is not authenticated and route requires auth (meta.requiresAuth), redirect to '/login'. Preserve the original path as a query parameter."
Generated code snippet:
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/login', component: () => import('@/views/Login.vue') },
{ path: '/dashboard', component: () => import('@/views/Dashboard.vue'), meta: { requiresAuth: true } },
],
});
router.beforeEach((to, from, next) => {
const authStore = useAuthStore();
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next({ path: '/login', query: { redirect: to.fullPath } });
} else {
next();
}
});
Why it works: This pattern is standard for protected routes. The redirect query allows seamless login flow.
6. Prompt: Reusable Form Input Component with Validation
Create a custom input component that integrates with v-model and displays validation errors.
Example prompt:
"Build a Vue 3 component FormInput with props: label, modelValue, type (default 'text'), and error. Emit update:modelValue. Show error message below the input in red. Use scoped styles."
Generated code snippet:
<template>
<div class="form-input">
<label :for="label">{{ label }}</label>
<input
:id="label"
:type="type"
:value="modelValue"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
:class="{ 'is-invalid': error }"
/>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
<script setup lang="ts">
defineProps<{
label: string;
modelValue: string;
type?: string;
error?: string;
}>();
defineEmits(['update:modelValue']);
</script>
<style scoped>
.form-input { margin-bottom: 1rem; }
label { display: block; margin-bottom: 0.25rem; font-weight: 500; }
input { width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; }
.is-invalid { border-color: #e74c3c; }
.error { color: #e74c3c; font-size: 0.875rem; margin-top: 0.25rem; }
</style>
Why it works: The v-model pattern is Vue's core two-way binding mechanism. This component is reusable across forms.
7. Prompt: Nuxt 3 Server Route with Database Query
Create a server API endpoint in Nuxt 3 that fetches data from a database (e.g., Prisma).
Example prompt:
"Write a Nuxt 3 server route server/api/posts.get.ts that queries all posts from Prisma, orders by creation date descending, and returns them. Use defineEventHandler."
Generated code snippet:
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default defineEventHandler(async () => {
try {
const posts = await prisma.post.findMany({
orderBy: { createdAt: 'desc' },
select: { id: true, title: true, excerpt: true, createdAt: true },
});
return { posts };
} catch (error) {
throw createError({ statusCode: 500, statusMessage: 'Database error' });
}
});
Why it works: Nuxt 3 server routes are auto-imported and run in Nitro, the server engine. createError provides structured error responses.
8. Prompt: Vue Teleport for Modal Component
Implement a modal component using Vue's Teleport to mount it outside the main app root.
Example prompt:
"Create a Modal component that uses <Teleport to="body">. Accept props: visible (boolean), title. Emit close when backdrop is clicked. Include a slot for body content. Use transitions for enter/leave."
Generated code snippet:
<template>
<Teleport to="body">
<Transition name="modal">
<div v-if="visible" class="modal-backdrop" @click.self="$emit('close')">
<div class="modal-content">
<header>
<h3>{{ title }}</h3>
<button @click="$emit('close')">×</button>
</header>
<main>
<slot />
</main>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
defineProps<{ visible: boolean; title: string }>();
defineEmits(['close']);
</script>
<style scoped>
.modal-backdrop {
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: 1.5rem; border-radius: 8px; min-width: 300px; }
.modal-enter-active, .modal-leave-active { transition: opacity 0.3s; }
</style>
Why it works: Teleport prevents z-index issues by rendering the modal at the document body level.
9. Prompt: Composable for Debounced Input
Create a composable that returns a debounced value for search inputs.
Example prompt:
"Write a composable useDebounce that takes a ref and a delay (default 300ms). Return a debounced ref that updates only after the delay. Use watch and setTimeout."
Generated code snippet:
import { ref, watch, type Ref } from 'vue';
export function useDebounce<T>(value: Ref<T>, delay: number = 300): Ref<T> {
const debouncedValue = ref(value.value) as Ref<T>;
let timeoutId: ReturnType<typeof setTimeout>;
watch(value, (newVal) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
debouncedValue.value = newVal;
}, delay);
});
return debouncedValue;
}
Why it works: Debouncing reduces API calls during rapid user input, improving performance.
10. Prompt: Nuxt 3 Middleware for Route Protection
Create a route middleware in Nuxt 3 that checks authentication before page load.
Example prompt:
"Write a Nuxt 3 middleware auth.ts in the middleware/ folder. Use useAuthStore to check if user is logged in. If not, redirect to '/login' with a query parameter for return URL. Use navigateTo."
Generated code snippet:
export default defineNuxtRouteMiddleware((to) => {
const authStore = useAuthStore();
if (!authStore.isAuthenticated) {
return navigateTo({
path: '/login',
query: { redirect: to.fullPath },
});
}
});
Why it works: Nuxt middleware runs before page rendering, preventing unauthorized access at the route level.
11. Prompt: Vue Directive for Click Outside
Create a custom directive that detects clicks outside an element.
Example prompt:
"Write a Vue 3 custom directive v-click-outside that takes a callback function. Use addEventListener on the document and check if the click target is outside the element. Clean up in unmounted."
Generated code snippet:
import type { Directive } from 'vue';
export const vClickOutside: Directive<HTMLElement, () => void> = {
mounted(el, binding) {
const handler = (event: MouseEvent) => {
if (!el.contains(event.target as Node)) {
binding.value();
}
};
el._clickOutsideHandler = handler;
document.addEventListener('click', handler);
},
unmounted(el) {
document.removeEventListener('click', el._clickOutsideHandler);
},
};
Why it works: This directive is useful for dropdowns and popups. It cleans up listeners to prevent memory leaks.
12. Prompt: Pinia Store with Local Storage Persistence
Extend a Pinia store to persist state to localStorage.
Example prompt:
"Modify the useTodoStore to automatically save and load todos from localStorage. Use watch to sync changes. Use a unique key for storage."
Generated code snippet:
import { defineStore } from 'pinia';
import { ref, watch } from 'vue';
const STORAGE_KEY = 'todos';
export const useTodoStore = defineStore('todo', () => {
const saved = localStorage.getItem(STORAGE_KEY);
const todos = ref(saved ? JSON.parse(saved) : []);
watch(todos, (newTodos) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(newTodos));
}, { deep: true });
// ... actions
return { todos };
});
Why it works: Persistence ensures state survives page refreshes. The deep watcher detects nested changes.
13. Prompt: Nuxt 3 Plugin for Global Error Handling
Create a Nuxt plugin that catches Vue errors globally and logs them.
Example prompt:
"Write a Nuxt plugin error-handler.ts that uses vueApp.config.errorHandler. Log errors to console and optionally send to a monitoring service. Use defineNuxtPlugin."
Generated code snippet:
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.config.errorHandler = (err, instance, info) => {
console.error('Global error:', err);
console.log('Component:', instance);
console.log('Info:', info);
// Example: send to Sentry
// captureException(err);
};
});
Why it works: Global error handlers catch unhandled exceptions, improving debuggability in production.
14. Prompt: Composable for Window Resize Listener
Create a composable that provides reactive window dimensions.
Example prompt:
"Write a composable useWindowSize that returns width and height refs, updated on window resize. Use addEventListener and clean up in onUnmounted."
Generated code snippet:
import { ref, onMounted, onUnmounted } from 'vue';
export function useWindowSize() {
const width = ref(window.innerWidth);
const height = ref(window.innerHeight);
const handler = () => {
width.value = window.innerWidth;
height.value = window.innerHeight;
};
onMounted(() => window.addEventListener('resize', handler));
onUnmounted(() => window.removeEventListener('resize', handler));
return { width, height };
}
Why it works: Reactive dimensions enable responsive components without manual calculations.
Conclusion
These 14 prompts cover the most common patterns in Vue.js and Nuxt development: typed components, composables, Pinia stores, routing guards, and server routes. Each prompt is designed to be adapted to your specific project needs. The key is to customize the generated code with your own business logic and types. Start by integrating a few prompts into your daily workflow—you'll notice immediate productivity gains. For more advanced patterns, refer to the official Vue.js and Nuxt documentation.
If you found this collection useful, share it with your team. Happy coding!
Comments