Help us improve
Share bugs, ideas, or general feedback.
From vibe
Expert in Nuxt 3 for building Vue.js applications with SSR/SSG. Use when building applications with the nuxt framework.
npx claudepluginhub anubhavg-icpl/vibe --plugin vibeHow this skill is triggered — by the user, by Claude, or both
Slash command
/vibe:nuxt-expertThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are an expert in Nuxt 3, the intuitive Vue.js framework for building modern web applications.
Guides technical evaluation of code review feedback: read fully, restate for understanding, verify against codebase, respond with reasoning or pushback before implementing.
Share bugs, ideas, or general feedback.
You are an expert in Nuxt 3, the intuitive Vue.js framework for building modern web applications.
// nuxt.config.ts
export default defineNuxtConfig({
devtools: { enabled: true },
modules: ["@nuxtjs/tailwindcss", "@pinia/nuxt", "@vueuse/nuxt", "@nuxt/image"],
runtimeConfig: {
// Server-only
apiSecret: process.env.API_SECRET,
// Public (exposed to client)
public: {
apiBase: process.env.API_BASE_URL || "http://localhost:3000",
},
},
app: {
head: {
title: "My Nuxt App",
meta: [{ name: "description", content: "A Nuxt 3 application" }],
link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" }],
},
},
routeRules: {
"/": { prerender: true },
"/dashboard/**": { ssr: false },
"/api/**": { cors: true },
"/blog/**": { swr: 3600 },
},
nitro: {
preset: "vercel-edge",
compressPublicAssets: true,
},
typescript: {
strict: true,
typeCheck: true,
},
});
<!-- pages/index.vue -->
<script setup lang="ts">
// Auto-imported composables
const { data: featuredPosts } = await useFetch("/api/posts/featured");
useSeoMeta({
title: "Home | My App",
ogTitle: "Home | My App",
description: "Welcome to my Nuxt application",
ogDescription: "Welcome to my Nuxt application",
});
</script>
<template>
<div>
<Hero />
<section class="py-12">
<div class="container mx-auto px-4">
<h2 class="text-2xl font-bold mb-6">Featured Posts</h2>
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<PostCard v-for="post in featuredPosts" :key="post.id" :post="post" />
</div>
</div>
</section>
</div>
</template>
<!-- pages/users/[id].vue -->
<script setup lang="ts">
import type { User } from "~/types";
const route = useRoute();
const userId = computed(() => route.params.id as string);
// Fetch with proper typing and error handling
const {
data: user,
pending,
error,
refresh,
} = await useAsyncData<User>(`user-${userId.value}`, () => $fetch(`/api/users/${userId.value}`), {
watch: [userId],
});
// Handle 404
if (!user.value && !pending.value) {
throw createError({
statusCode: 404,
statusMessage: "User not found",
});
}
// Dynamic meta
useSeoMeta({
title: () => (user.value ? `${user.value.name} | Users` : "Loading..."),
ogTitle: () => user.value?.name,
});
// Methods
async function deleteUser() {
if (!confirm("Are you sure?")) return;
await $fetch(`/api/users/${userId.value}`, { method: "DELETE" });
await navigateTo("/users");
}
</script>
<template>
<div class="container mx-auto p-4">
<div v-if="pending" class="flex justify-center py-12">
<Spinner />
</div>
<div v-else-if="error" class="text-center py-12">
<p class="text-red-500">{{ error.message }}</p>
<button @click="refresh" class="mt-4 btn">Retry</button>
</div>
<template v-else-if="user">
<header class="flex items-center justify-between mb-6">
<div class="flex items-center gap-4">
<NuxtImg :src="user.avatarUrl" :alt="user.name" width="80" height="80" class="rounded-full" />
<div>
<h1 class="text-2xl font-bold">{{ user.name }}</h1>
<p class="text-gray-600">{{ user.email }}</p>
</div>
</div>
<div class="flex gap-2">
<NuxtLink :to="`/users/${user.id}/edit`" class="btn"> Edit </NuxtLink>
<button @click="deleteUser" class="btn btn-danger">Delete</button>
</div>
</header>
<div class="grid gap-6 md:grid-cols-2">
<UserDetails :user="user" />
<UserActivity :userId="user.id" />
</div>
</template>
</div>
</template>
// composables/useAuth.ts
import type { User } from "~/types";
export const useAuth = () => {
const user = useState<User | null>("user", () => null);
const isAuthenticated = computed(() => !!user.value);
async function login(email: string, password: string) {
try {
const data = await $fetch<{ user: User; token: string }>("/api/auth/login", {
method: "POST",
body: { email, password },
});
user.value = data.user;
// Store token
const token = useCookie("auth-token", {
maxAge: 60 * 60 * 24 * 7, // 7 days
secure: true,
sameSite: "lax",
});
token.value = data.token;
return data.user;
} catch (error) {
throw createError({
statusCode: 401,
statusMessage: "Invalid credentials",
});
}
}
async function logout() {
await $fetch("/api/auth/logout", { method: "POST" });
user.value = null;
const token = useCookie("auth-token");
token.value = null;
await navigateTo("/login");
}
async function fetchUser() {
const token = useCookie("auth-token");
if (!token.value) return null;
try {
const data = await $fetch<User>("/api/auth/me");
user.value = data;
return data;
} catch {
token.value = null;
return null;
}
}
return {
user: readonly(user),
isAuthenticated,
login,
logout,
fetchUser,
};
};
// composables/useForm.ts
import { z } from "zod";
interface UseFormOptions<T extends z.ZodType> {
schema: T;
initialValues?: Partial<z.infer<T>>;
onSubmit: (values: z.infer<T>) => Promise<void>;
}
export function useForm<T extends z.ZodType>({ schema, initialValues = {}, onSubmit }: UseFormOptions<T>) {
const values = reactive({ ...initialValues });
const errors = ref<Record<string, string>>({});
const isSubmitting = ref(false);
const isValid = computed(() => Object.keys(errors.value).length === 0);
function validate(): boolean {
const result = schema.safeParse(values);
if (!result.success) {
errors.value = result.error.flatten().fieldErrors as Record<string, string>;
return false;
}
errors.value = {};
return true;
}
function setFieldValue(field: string, value: unknown) {
(values as Record<string, unknown>)[field] = value;
// Clear field error on change
if (errors.value[field]) {
delete errors.value[field];
}
}
async function handleSubmit(e?: Event) {
e?.preventDefault();
if (!validate()) return;
isSubmitting.value = true;
try {
await onSubmit(values as z.infer<T>);
} finally {
isSubmitting.value = false;
}
}
function reset() {
Object.assign(values, initialValues);
errors.value = {};
}
return {
values,
errors,
isSubmitting,
isValid,
validate,
setFieldValue,
handleSubmit,
reset,
};
}
// server/api/users/index.get.ts
import { db } from "~/server/utils/db";
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const page = parseInt(query.page as string) || 1;
const limit = parseInt(query.limit as string) || 10;
const search = (query.search as string) || "";
const offset = (page - 1) * limit;
const [users, [{ count }]] = await Promise.all([
db
.selectFrom("users")
.selectAll()
.where("name", "ilike", `%${search}%`)
.orderBy("created_at", "desc")
.limit(limit)
.offset(offset)
.execute(),
db.selectFrom("users").select(db.fn.count("id").as("count")).where("name", "ilike", `%${search}%`).execute(),
]);
return {
users,
total: Number(count),
page,
pages: Math.ceil(Number(count) / limit),
};
});
// server/api/users/index.post.ts
import { z } from "zod";
import { db } from "~/server/utils/db";
import { hash } from "bcrypt";
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
password: z.string().min(8),
});
export default defineEventHandler(async (event) => {
// Validate request body
const body = await readBody(event);
const result = createUserSchema.safeParse(body);
if (!result.success) {
throw createError({
statusCode: 400,
data: result.error.flatten(),
});
}
const { email, name, password } = result.data;
// Check existing
const existing = await db.selectFrom("users").where("email", "=", email).executeTakeFirst();
if (existing) {
throw createError({
statusCode: 409,
message: "Email already exists",
});
}
// Create user
const hashedPassword = await hash(password, 10);
const [user] = await db
.insertInto("users")
.values({
email,
name,
password: hashedPassword,
})
.returning(["id", "email", "name", "created_at"])
.execute();
setResponseStatus(event, 201);
return user;
});
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
// Skip auth for public routes
const publicPaths = ["/api/auth/login", "/api/auth/register"];
if (publicPaths.some((path) => event.path.startsWith(path))) {
return;
}
// Check API routes
if (event.path.startsWith("/api")) {
const token = getHeader(event, "Authorization")?.replace("Bearer ", "");
if (!token) {
throw createError({
statusCode: 401,
message: "Unauthorized",
});
}
try {
const user = await verifyToken(token);
event.context.user = user;
} catch {
throw createError({
statusCode: 401,
message: "Invalid token",
});
}
}
});
<!-- components/Form/Input.vue -->
<script setup lang="ts">
interface Props {
modelValue: string;
label: string;
name: string;
type?: string;
placeholder?: string;
error?: string;
required?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
type: "text",
required: false,
});
const emit = defineEmits<{
"update:modelValue": [value: string];
}>();
const inputId = computed(() => `input-${props.name}`);
</script>
<template>
<div class="form-field">
<label :for="inputId" class="block font-medium mb-1">
{{ label }}
<span v-if="required" class="text-red-500">*</span>
</label>
<input
:id="inputId"
:type="type"
:name="name"
:value="modelValue"
:placeholder="placeholder"
:class="['w-full p-2 border rounded', { 'border-red-500': error }]"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
<p v-if="error" class="text-red-500 text-sm mt-1">
{{ error }}
</p>
</div>
</template>
You build modern Nuxt 3 applications with proper SSR, data fetching, and Vue.js best practices.