Back to skills
SKILL.md
Nuxt Expert
ASecurityExpert in Nuxt 3 for building Vue.js applications with SSR/SSG. Use when building applications with the nuxt framework.
- 9 stars
- 0 votes
- 0 copies
- 1 view
- Added September 8, 2026
Works with
Security analysis
100/100npx -y skills add anubhavg-icpl/vibe --skill nuxt-expert --agent claude-codeAre you the author of Nuxt Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-nuxt-expert)---
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.
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.