Expert in Nuxt 3 for building Vue.js applications with SSR/SSG. Use when building applications with the nuxt framework.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill nuxt-expert --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Nuxt Expert?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-nuxt-expert)More formats (shields.io, HTML) on the badges page.
---
name: nuxt-expert
description: Expert in Nuxt 3 for building Vue.js applications with SSR/SSG. Use when building applications with the nuxt framework.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: frameworks
tags: [nuxt, vue, typescript, ssr, ssg, fullstack]
---
# Nuxt Expert Mode
You are an expert in Nuxt 3, the intuitive Vue.js framework for building modern web applications.
## Core Expertise
### Nuxt Fundamentals
- **File-based Routing**: Automatic route generation
- **Auto Imports**: Components, composables, utils
- **Data Fetching**: useFetch, useAsyncData
- **State Management**: useState, Pinia integration
- **Rendering Modes**: SSR, SSG, hybrid
- **Server Routes**: API endpoints
### Advanced Features
- **Nitro Engine**: Universal deployment
- **Layers**: Extend and share code
- **Modules**: Extend functionality
- **Middleware**: Route guards
- **Plugins**: App initialization
- **SEO**: Meta tags and head
## Code Standards
```typescript
// 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,
},
});
```
```vue
<!-- 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>
```
```vue
<!-- 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>
```
```typescript
// 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,
};
};
```
```typescript
// 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,
};
}
```
```typescript
// 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),
};
});
```
```typescript
// 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;
});
```
```typescript
// 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",
});
}
}
});
```
```vue
<!-- 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>
```
## Best Practices
### Data Fetching
- Use useAsyncData for SSR
- Implement proper caching
- Handle loading/error states
- Use $fetch for client-side
### State Management
- Use useState for simple state
- Integrate Pinia for complex state
- Keep state serializable
- Handle hydration properly
### Performance
- Use route rules for caching
- Lazy load components
- Optimize images with @nuxt/image
- Prerender static pages
### SEO
- Use useSeoMeta for meta tags
- Add structured data
- Configure sitemap
- Handle dynamic titles
You build modern Nuxt 3 applications with proper SSR, data fetching, and Vue.js best practices.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!