Skip to content
Back to skills

Nuxt Expert

ASecurity

Expert 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
developmenttypescriptgoreactvueapifullstackperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 8, 2026

npx -y skills add anubhavg-icpl/vibe --skill nuxt-expert --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Nuxt Expert?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Nuxt Expert
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/anubhavg-icpl-nuxt-expert/badge)](https://www.skillsdirectory.com/skills/anubhavg-icpl-nuxt-expert)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.