nuxt 4 frontend development

Nuxt 4 Frontend Development Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "nuxt 4 frontend development" with this command: npx skills add pstuart/pstuart/pstuart-pstuart-nuxt-4-frontend-development

Nuxt 4 Frontend Development Skill

Overview

This skill guides development of modern, type-safe frontend applications using:

  • Nuxt 4 - Latest version with app/ directory structure, improved TypeScript support and auto-imports

  • Vue 3 - Composition API with <script setup> syntax

  • TypeScript - Strict mode enabled for maximum type safety

  • Vitest - Fast unit testing with Vue Test Utils

  • ESLint - Code quality and consistency

  • Tailwind CSS 4 - Utility-first styling with Vite plugin (NO inline styles or custom CSS allowed)

When to Use This Skill

Use this skill when:

  • Creating new Nuxt 4 components, composables, or utilities in the app/ directory

  • Writing tests for Vue components or composables

  • Setting up or modifying project configuration

  • Implementing new features following project conventions

  • Styling components (MUST use Tailwind classes only, NO inline styles or custom CSS)

  • Organizing code with Nuxt layers

  • Setting up Tailwind 4 with the Vite plugin

Project Structure

Directory Organization

Nuxt 4 uses the app/ directory as the primary source directory. All application code lives under app/ :

project/ ├── app/ │ ├── components/ │ │ ├── ui/ # Reusable UI components (buttons, inputs, etc.) │ │ ├── features/ # Feature-specific components │ │ └── layouts/ # Layout components │ ├── composables/ # Reusable composition functions │ ├── utils/ # Pure utility functions │ ├── types/ # TypeScript type definitions │ ├── pages/ # File-based routing │ ├── layouts/ # Application layouts │ ├── middleware/ # Route middleware │ ├── plugins/ # Vue plugins │ ├── assets/ # Assets to be processed (CSS, images) │ │ └── css/ │ │ └── main.css # Tailwind imports │ └── app.vue # Root application component ├── server/ # Server API routes and middleware (stays at root) │ ├── api/ │ ├── middleware/ │ └── utils/ ├── layers/ # Nuxt layers for code organization │ └── base/ # Example: shared base layer ├── tests/ # Test utilities and fixtures │ ├── unit/ │ └── integration/ ├── public/ # Static assets (not processed) └── nuxt.config.ts # Nuxt configuration

Key Points:

  • All application code goes in app/

  • components, composables, pages, layouts, etc.

  • Server code stays at root level in server/ directory

  • Use app/assets/ for assets that need processing (Tailwind CSS, images)

  • Use public/ for static files served as-is

Naming Conventions

  • Components: PascalCase (e.g., app/components/UserProfile.vue , app/components/ui/BaseButton.vue )

  • Composables: camelCase with use prefix (e.g., app/composables/useAuth.ts , app/composables/useFetchData.ts )

  • Utils: camelCase (e.g., app/utils/formatDate.ts , app/utils/validateEmail.ts )

  • Types: PascalCase for interfaces/types (e.g., User , ApiResponse )

  • Pages: kebab-case (e.g., app/pages/user-profile.vue , app/pages/about-us.vue )

  • Layouts: kebab-case (e.g., app/layouts/default.vue , app/layouts/admin.vue )

  • Test files: Match source file with .test.ts or .spec.ts suffix

Nuxt Layers

Nuxt layers allow you to organize and share code across projects. Use layers for:

  • Shared UI components and composables

  • Base configuration and setup

  • Theme systems

  • Multi-tenant applications

Layer Structure:

layers/ ├── base/ # Shared base layer │ ├── app/ │ │ ├── components/ │ │ ├── composables/ │ │ └── utils/ │ └── nuxt.config.ts # Layer-specific config └── admin/ # Admin-specific layer ├── app/ │ ├── components/ │ └── pages/ └── nuxt.config.ts

Using Layers in nuxt.config.ts:

export default defineNuxtConfig({ extends: [ './layers/base', './layers/admin' ] })

Layer Best Practices:

  • Each layer should have its own nuxt.config.ts

  • Layers can extend other layers

  • Components, composables, and utils from layers are auto-imported

  • Layer order matters - later layers override earlier ones

  • Keep layers focused on specific domains or features

  • Document layer dependencies clearly

Example Base Layer Config:

// layers/base/nuxt.config.ts export default defineNuxtConfig({ components: { dirs: [ { path: '~/components', global: true } ] } })

Code Standards

TypeScript Configuration

Always use strict TypeScript settings:

// nuxt.config.ts export default defineNuxtConfig({ typescript: { strict: true, typeCheck: true, } })

Type Definition Best Practices:

  • Define interfaces in types/ directory for shared types

  • Use type for unions, intersections, and simple aliases

  • Use interface for object shapes that may be extended

  • Export types from a central types/index.ts for easy imports

  • Always type function parameters and return values

Vue 3 Component Patterns

Preferred Component Structure:

<script setup lang="ts"> // 1. Imports import { ref, computed, onMounted } from 'vue' import type { User } from '~/types'

// 2. Props and Emits interface Props { userId: string isActive?: boolean }

interface Emits { update: [user: User] close: [] }

const props = withDefaults(defineProps<Props>(), { isActive: true })

const emit = defineEmits<Emits>()

// 3. Composables const { data: user, pending } = await useFetch(/api/users/${props.userId})

// 4. Reactive State const isEditing = ref(false)

// 5. Computed Properties const displayName = computed(() => user.value ? ${user.value.firstName} ${user.value.lastName} : '' )

// 6. Methods const handleUpdate = () => { if (user.value) { emit('update', user.value) } }

// 7. Lifecycle onMounted(() => { console.log('Component mounted') }) </script>

<template> <div class="container"> <!-- Template content --> </div> </template>

Key Principles:

  • Always use <script setup> for Composition API

  • Use lang="ts" on script tags

  • Destructure props carefully (use .value when needed)

  • Prefer computed over methods for derived state

  • Use defineProps with TypeScript interfaces, not runtime props

Composables Patterns

Structure for Reusable Composables:

// composables/useAuth.ts import { ref, computed } from 'vue' import type { User } from '~/types'

export const useAuth = () => { // State const user = ref<User | null>(null) const isLoading = ref(false) const error = ref<Error | null>(null)

// Computed const isAuthenticated = computed(() => !!user.value)

// Methods const login = async (email: string, password: string) => { isLoading.value = true error.value = null

try {
  const response = await $fetch&#x3C;User>('/api/auth/login', {
    method: 'POST',
    body: { email, password }
  })
  user.value = response
} catch (e) {
  error.value = e as Error
  throw e
} finally {
  isLoading.value = false
}

}

const logout = async () => { await $fetch('/api/auth/logout', { method: 'POST' }) user.value = null }

// Return public API return { user: readonly(user), isAuthenticated, isLoading: readonly(isLoading), error: readonly(error), login, logout } }

Composable Best Practices:

  • Always return an object with named properties

  • Use readonly() for state that shouldn't be mutated externally

  • Include loading and error states for async operations

  • Provide TypeScript types for all parameters and return values

  • Keep composables focused on a single responsibility

Data Fetching Patterns

Preferred Nuxt 4 Data Fetching:

// Good: Using useFetch with auto-typed response const { data, pending, error, refresh } = await useFetch('/api/users', { query: { limit: 10 } })

// Good: Using useAsyncData for more control const { data: users } = await useAsyncData( 'users-list', () => $fetch<User[]>('/api/users') )

// Good: Lazy loading with explicit type const { data, pending } = useLazyFetch<Product>(/api/products/${id})

Data Fetching Rules:

  • Use useFetch for simple API calls

  • Use useAsyncData when you need custom async logic

  • Add unique keys to useAsyncData to manage cache

  • Always await in <script setup> to enable SSR

  • Use useLazyFetch for client-side only or lazy loading

  • Type the response explicitly when TypeScript can't infer

Tailwind CSS Guidelines

CRITICAL: We use Tailwind 4 with the Vite plugin. ALWAYS use Tailwind utility classes. NEVER use inline styles or custom CSS that could be achieved with Tailwind classes.

Tailwind 4 Setup:

// nuxt.config.ts export default defineNuxtConfig({ vite: { plugins: [ // Tailwind 4 uses Vite plugin instead of PostCSS ] }, css: ['~/assets/css/main.css'] })

/* app/assets/css/main.css */ @import "tailwindcss";

/* Custom design tokens (if needed) */ @theme { --color-primary: #3b82f6; --color-secondary: #8b5cf6; --font-display: 'Inter', sans-serif; }

Mandatory Class Usage Rules:

❌ NEVER do this:

<!-- BAD: Inline styles --> <div style="padding: 16px; background-color: blue;">

<!-- BAD: Custom CSS that could be Tailwind --> <style scoped> .my-button { padding: 1rem; background-color: #3b82f6; border-radius: 0.5rem; } </style>

<!-- BAD: Arbitrary CSS properties --> <div class="[background:linear-gradient(to-right,#fff,#000)]">

✅ ALWAYS do this:

<!-- GOOD: Pure Tailwind classes --> <div class="p-4 bg-blue-500">

<button class="px-4 py-2 bg-blue-600 rounded-lg">

<!-- GOOD: Use Tailwind's gradient utilities --> <div class="bg-gradient-to-r from-white to-black">

Class Organization: Order classes by category for better readability:

  • Layout: flex , grid , block , inline-flex

  • Positioning: relative , absolute , top-0 , left-0

  • Spacing: p-4 , m-2 , space-x-4 , gap-4

  • Sizing: w-full , h-screen , max-w-lg

  • Typography: text-sm , font-bold , leading-tight

  • Colors: text-gray-900 , bg-blue-600

  • Borders: border , border-gray-300 , rounded-lg

  • Effects: shadow-lg , opacity-50

  • Transitions: transition-all , duration-300

  • States: hover:bg-blue-700 , focus:ring-2 , disabled:opacity-50

Example:

<template> <button class=" flex items-center justify-center px-6 py-3 w-full max-w-xs text-base font-semibold text-white bg-blue-600 rounded-xl shadow-md transition-all duration-200 hover:bg-blue-700 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600 " :disabled="isLoading"

&#x3C;LoadingSpinner v-if="isLoading" class="mr-2 h-4 w-4" />
{{ isLoading ? 'Processing...' : 'Submit' }}

</button> </template>

Responsive Design:

<template> <div class=" grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4 sm:p-6 lg:p-8 "> <!-- Content --> </div> </template>

When to Extract Components: If you're repeating the same class combinations across multiple components, extract them into a reusable component:

<!-- app/components/ui/PrimaryButton.vue --> <template> <button class=" px-6 py-3 text-base font-semibold text-white bg-blue-600 rounded-xl hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed " v-bind="$attrs"

&#x3C;slot />

</button> </template>

<script setup lang="ts"> defineOptions({ inheritAttrs: false }) </script>

DO NOT use @apply or custom CSS:

/* ❌ BAD - Don't do this */ <style scoped> .btn-primary { @apply px-6 py-3 bg-blue-600 text-white rounded-xl; } </style>

/* ✅ GOOD - Use Tailwind classes directly in template */

Tailwind 4 Configuration:

// tailwind.config.ts (if needed for custom values) export default { theme: { extend: { colors: { brand: { 50: '#eff6ff', 500: '#3b82f6', 900: '#1e3a8a', } }, spacing: { '128': '32rem', } } } }

Custom Properties with Tailwind 4: Use CSS custom properties in @theme for dynamic theming:

/* app/assets/css/main.css */ @import "tailwindcss";

@theme { --color-brand-primary: oklch(0.5 0.2 240); --color-brand-secondary: oklch(0.6 0.15 280); --radius-default: 0.5rem; }

Then use in templates:

<div class="bg-brand-primary text-white rounded-[--radius-default]">

Key Principles:

  • NEVER write custom CSS that could be Tailwind utilities

  • NEVER use inline style attributes

  • Extract repeated patterns to reusable components, NOT CSS

  • Use Tailwind's spacing scale (don't use arbitrary values unless absolutely necessary)

  • Leverage Tailwind's color palette - only extend when brand requires specific colors

  • Always use responsive prefixes for mobile-first design

ESLint Configuration

Expected ESLint Setup:

// .eslintrc.cjs or eslint.config.js module.exports = { extends: [ '@nuxt/eslint-config', 'plugin:vue/vue3-recommended', 'plugin:@typescript-eslint/recommended' ], rules: { 'vue/multi-word-component-names': 'error', 'vue/component-name-in-template-casing': ['error', 'PascalCase'], '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/explicit-function-return-type': 'off', 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off' } }

Testing with Vitest

Test File Structure

// components/UserCard.test.ts import { describe, it, expect, beforeEach } from 'vitest' import { mount } from '@vue/test-utils' import UserCard from './UserCard.vue' import type { User } from '~/types'

describe('UserCard', () => { const mockUser: User = { id: '1', name: 'John Doe', email: 'john@example.com' }

it('renders user information correctly', () => { const wrapper = mount(UserCard, { props: { user: mockUser } })

expect(wrapper.text()).toContain('John Doe')
expect(wrapper.text()).toContain('john@example.com')

})

it('emits delete event when delete button clicked', async () => { const wrapper = mount(UserCard, { props: { user: mockUser } })

await wrapper.find('[data-test="delete-btn"]').trigger('click')

expect(wrapper.emitted('delete')).toBeTruthy()
expect(wrapper.emitted('delete')?.[0]).toEqual([mockUser.id])

}) })

Composable Testing

// composables/useCounter.test.ts import { describe, it, expect } from 'vitest' import { useCounter } from './useCounter'

describe('useCounter', () => { it('increments counter', () => { const { count, increment } = useCounter()

expect(count.value).toBe(0)
increment()
expect(count.value).toBe(1)

})

it('decrements counter', () => { const { count, decrement } = useCounter(5)

expect(count.value).toBe(5)
decrement()
expect(count.value).toBe(4)

}) })

Testing Best Practices

  • Test user behavior, not implementation details

  • Use data-test attributes for reliable element selection

  • Mock external dependencies (APIs, composables)

  • Test edge cases and error states

  • Keep tests isolated and independent

  • Use describe blocks to organize related tests

  • Write descriptive test names that explain the expected behavior

Vitest Configuration

// vitest.config.ts import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' import { fileURLToPath } from 'node:url'

export default defineConfig({ plugins: [vue()], test: { environment: 'jsdom', globals: true, setupFiles: ['./tests/setup.ts'] }, resolve: { alias: { '~': fileURLToPath(new URL('./', import.meta.url)), '@': fileURLToPath(new URL('./', import.meta.url)) } } })

Common Patterns

Form Handling

<script setup lang="ts"> import { ref, reactive } from 'vue' import { z } from 'zod'

const schema = z.object({ email: z.string().email(), password: z.string().min(8) })

type FormData = z.infer<typeof schema>

const form = reactive<FormData>({ email: '', password: '' })

const errors = ref<Partial<Record<keyof FormData, string>>>({}) const isSubmitting = ref(false)

const handleSubmit = async () => { errors.value = {}

const result = schema.safeParse(form) if (!result.success) { result.error.issues.forEach(issue => { if (issue.path[0]) { errors.value[issue.path[0] as keyof FormData] = issue.message } }) return }

isSubmitting.value = true try { await $fetch('/api/auth/login', { method: 'POST', body: form }) } catch (e) { console.error(e) } finally { isSubmitting.value = false } } </script>

Error Handling

// utils/error-handler.ts export class ApiError extends Error { constructor( message: string, public statusCode: number, public code?: string ) { super(message) this.name = 'ApiError' } }

export const handleApiError = (error: unknown): string => { if (error instanceof ApiError) { return error.message }

if (error instanceof Error) { return error.message }

return 'An unexpected error occurred' }

Loading States

<script setup lang="ts"> const { data, pending, error } = await useFetch('/api/data') </script>

<template> <div> <div v-if="pending" class="flex justify-center p-8"> <LoadingSpinner /> </div>

&#x3C;div v-else-if="error" class="text-red-600">
  {{ error.message }}
&#x3C;/div>

&#x3C;div v-else-if="data">
  &#x3C;!-- Render data -->
&#x3C;/div>

</div> </template>

Anti-Patterns (Don't Do This)

❌ Avoid Options API

<!-- BAD --> <script lang="ts"> export default { data() { return { count: 0 } }, methods: { increment() { this.count++ } } } </script>

<!-- GOOD --> <script setup lang="ts"> const count = ref(0) const increment = () => count.value++ </script>

❌ Don't Use any Type

// BAD const fetchData = async (): Promise<any> => { ... }

// GOOD const fetchData = async (): Promise<User[]> => { ... }

❌ Avoid Mutating Props

<script setup lang="ts"> const props = defineProps<{ count: number }>()

// BAD const increment = () => props.count++

// GOOD const emit = defineEmits<{ increment: [] }>() const increment = () => emit('increment') </script>

❌ NEVER Use Inline Styles

<!-- BAD - Inline styles are forbidden --> <div style="padding: 16px; background-color: blue;"> <div :style="{ padding: '16px', backgroundColor: 'blue' }"> <div :style="computedStyles">

<!-- GOOD - Always use Tailwind classes --> <div class="p-4 bg-blue-500">

❌ NEVER Write Custom CSS for Tailwind-Available Styles

<!-- BAD - Don't write CSS that Tailwind already provides --> <style scoped> .my-container { display: flex; align-items: center; padding: 1rem; background-color: #3b82f6; border-radius: 0.5rem; } </style>

<!-- GOOD - Use Tailwind utilities --> <div class="flex items-center p-4 bg-blue-500 rounded-lg">

❌ Don't Use @apply Directive

<!-- BAD - Avoid @apply, use classes directly --> <style scoped> .btn { @apply px-4 py-2 bg-blue-600 text-white rounded-lg; } </style>

<!-- GOOD - Component extraction for reuse --> <!-- app/components/ui/Button.vue --> <template> <button class="px-4 py-2 bg-blue-600 text-white rounded-lg"> <slot /> </button> </template>

❌ Don't Use Nuxt 2 Patterns

// BAD (Nuxt 2) export default { asyncData({ $axios }) { return $axios.get('/api/users') } }

// GOOD (Nuxt 4) const { data: users } = await useFetch('/api/users')

❌ Avoid Deep Prop Drilling

// BAD - passing data through many layers <ComponentA :user="user" /> <ComponentB :user="user" /> <ComponentC :user="user" />

// GOOD - use composables or provide/inject // In parent provide('user', user)

// In deep child const user = inject<User>('user')

❌ Don't Use Old Folder Structure

// BAD (Nuxt 3 and earlier) components/MyComponent.vue composables/useAuth.ts pages/index.vue

// GOOD (Nuxt 4) app/components/MyComponent.vue app/composables/useAuth.ts app/pages/index.vue

Quick Reference Commands

Development

npm run dev

Type checking

npm run typecheck

Linting

npm run lint npm run lint:fix

Testing

npm run test npm run test:watch npm run test:coverage

Build

npm run build npm run preview

Additional Notes

  • Always run npm run typecheck before committing

  • Use console.log sparingly; prefer debugging tools

  • Keep components under 200 lines; split if larger

  • Write tests for all business logic and complex components

  • Document complex logic with comments

  • Use TypeScript's utility types (Partial , Pick , Omit , etc.)

  • CRITICAL: NEVER use inline styles (style attribute or :style binding)

  • CRITICAL: NEVER write custom CSS that could be Tailwind utility classes

  • CRITICAL: All styling MUST use Tailwind utility classes directly in templates

  • All application code goes in the app/ directory (Nuxt 4 convention)

  • Use Nuxt layers for shared code and multi-tenant applications

  • Extract repeated Tailwind patterns into reusable components, not CSS classes

Resources

  • Nuxt 4 Documentation

  • Vue 3 Composition API

  • Vitest Documentation

  • Tailwind CSS

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

cross-platform app development skill

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

xcode build & simulator

No summary provided by upstream source.

Repository SourceNeeds Review
General

book-publisher

No summary provided by upstream source.

Repository SourceNeeds Review
nuxt 4 frontend development | V50.AI