Ejemplos de Nuxt

Ejemplos del framework full-stack Vue Nuxt.js incluyendo SSR, SSG, rutas API y patrones modernos de desarrollo Vue

💻 Aplicación Básica de Nuxt typescript

🟢 simple ⭐⭐

Estructura esencial de aplicación Nuxt con páginas, componentes, layouts y configuración

⏱️ 30 min 🏷️ nuxt, vue, full-stack, ssr
Prerequisites: Vue.js basics, JavaScript/TypeScript, Node.js
// Nuxt 3 Basic Application Examples
// Nuxt is an intuitive full-stack web framework built on top of Vue.js

// 1. Nuxt Configuration (nuxt.config.ts)
export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: [
    '@nuxtjs/tailwindcss',
    '@pinia/nuxt',
    '@vueuse/nuxt'
  ],
  css: ['~/assets/css/main.css'],
  runtimeConfig: {
    // Private keys (only available on server-side)
    apiSecret: process.env.API_SECRET,
    // Public keys (exposed to client-side)
    public: {
      apiBase: process.env.API_BASE || '/api'
    }
  },
  nitro: {
    experimental: {
      wasm: true
    }
  }
});

// 2. App Entry Point (app.vue)
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

// 3. Default Layout (layouts/default.vue)
<template>
  <div class="min-h-screen bg-gray-50">
    <nav class="bg-white shadow-sm border-b">
      <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div class="flex justify-between h-16">
          <div class="flex items-center">
            <NuxtLink to="/" class="text-xl font-bold text-gray-900">
              My App
            </NuxtLink>
          </div>
          <div class="flex items-center space-x-4">
            <NuxtLink to="/posts" class="text-gray-600 hover:text-gray-900">
              Posts
            </NuxtLink>
            <NuxtLink to="/about" class="text-gray-600 hover:text-gray-900">
              About
            </NuxtLink>
          </div>
        </div>
      </div>
    </nav>

    <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
      <slot />
    </main>

    <footer class="bg-white border-t mt-12">
      <div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
        <p class="text-center text-gray-500">
          © 2024 My App. Built with Nuxt 3.
        </p>
      </div>
    </footer>
  </div>
</template>

// 4. Index Page (pages/index.vue)
<template>
  <div class="space-y-6">
    <div class="text-center">
      <h1 class="text-4xl font-bold text-gray-900 mb-4">
        Welcome to Nuxt 3
      </h1>
      <p class="text-xl text-gray-600">
        The Intuitive Vue Framework
      </p>
    </div>

    <div class="grid md:grid-cols-3 gap-6">
      <UCard>
        <template #header>
          <h3 class="text-lg font-semibold">Server-Side Rendering</h3>
        </template>
        <p class="text-gray-600">
          Nuxt provides server-side rendering out of the box for better SEO and performance.
        </p>
      </UCard>

      <UCard>
        <template #header>
          <h3 class="text-lg font-semibold">Auto-imports</h3>
        </template>
        <p class="text-gray-600">
          Vue components, composables, and utilities are automatically imported.
        </p>
      </UCard>

      <UCard>
        <template #header>
          <h3 class="text-lg font-semibold">File-based Routing</h3>
        </template>
        <p class="text-gray-600">
          Create routes automatically based on your file structure.
        </p>
      </UCard>
    </div>

    <div class="text-center">
      <UButton to="/posts" size="xl">
        Get Started
      </UButton>
    </div>
  </div>
</template>

<script setup>
// SEO Meta
useSeoMeta({
  title: 'Welcome to Nuxt 3',
  description: 'Get started with Nuxt 3, the intuitive Vue framework',
  ogTitle: 'Nuxt 3 Starter',
  ogDescription: 'A modern Nuxt 3 application template',
  ogImage: '/og-image.jpg'
})

// Page meta
definePageMeta({
  title: 'Home',
  description: 'Welcome page'
})
</script>

// 5. Dynamic Route Page (pages/posts/[id].vue)
<template>
  <div class="max-w-4xl mx-auto">
    <div v-if="pending" class="text-center py-8">
      <USpinner />
      <p class="mt-2 text-gray-600">Loading post...</p>
    </div>

    <div v-else-if="error" class="text-center py-8">
      <UAlert color="red" icon="i-heroicons-exclamation-triangle">
        <template #title>Error loading post</template>
        <template #description>{{ error.message }}</template>
      </UAlert>
    </div>

    <article v-else-if="data" class="prose lg:prose-xl">
      <header class="mb-8">
        <h1 class="text-4xl font-bold mb-4">{{ data.title }}</h1>
        <div class="flex items-center space-x-4 text-gray-600">
          <span>{{ formatDate(data.createdAt) }}</span>
          <span>•</span>
          <span>{{ data.author }}</span>
        </div>
      </header>

      <div class="prose-content" v-html="data.content"></div>

      <footer class="mt-12 pt-8 border-t">
        <div class="flex items-center justify-between">
          <div class="flex items-center space-x-2">
            <UAvatar :src="data.authorAvatar" :alt="data.author" />
            <span class="font-medium">{{ data.author }}</span>
          </div>
          <div class="flex items-center space-x-4">
            <UButton to="/posts" variant="outline" size="sm">
              ← Back to Posts
            </UButton>
          </div>
        </div>
      </footer>
    </article>
  </div>
</template>

<script setup>
// Route params
const route = useRoute()
const postId = route.params.id

// Fetch post data
const { data, pending, error } = await useFetch(`/api/posts/${postId}`)

// Format date utility
function formatDate(dateString: string) {
  return new Date(dateString).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  })
}

// SEO
if (data.value) {
  useSeoMeta({
    title: data.value.title,
    description: data.value.excerpt,
    ogTitle: data.value.title,
    ogDescription: data.value.excerpt,
    ogImage: data.value.featuredImage
  })
}

// Page meta
definePageMeta({
  title: 'Post Detail',
  description: 'Read our blog post'
})
</script>

// 6. API Route (server/api/posts/[id].get.ts)
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')

  // Mock database - in real app, use Prisma, Drizzle, etc.
  const posts = [
    {
      id: '1',
      title: 'Getting Started with Nuxt 3',
      excerpt: 'Learn the basics of Nuxt 3 and build your first application',
      content: `
        <h2>What is Nuxt 3?</h2>
        <p>Nuxt 3 is a full-stack web framework built on top of Vue.js...</p>
        <h2>Key Features</h2>
        <ul>
          <li>Server-Side Rendering (SSR)</li>
          <li>Static Site Generation (SSG)</li>
          <li>Auto-imports</li>
          <li>File-based routing</li>
        </ul>
      `,
      author: 'John Doe',
      authorAvatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e',
      createdAt: '2024-01-15',
      featuredImage: '/posts/getting-started-nuxt3.jpg'
    },
    {
      id: '2',
      title: 'Vue 3 Composition API Deep Dive',
      excerpt: 'Master the Composition API and build reactive applications',
      content: `
        <h2>Understanding Composition API</h2>
        <p>The Composition API provides a more flexible way to organize logic...</p>
      `,
      author: 'Jane Smith',
      authorAvatar: 'https://images.unsplash.com/photo-1494790108755-2616b332c1ca',
      createdAt: '2024-01-20',
      featuredImage: '/posts/vue3-composition-api.jpg'
    }
  ]

  const post = posts.find(p => p.id === id)

  if (!post) {
    throw createError({
      statusCode: 404,
      statusMessage: 'Post not found'
    })
  }

  return post
})

// 7. Form Handling with Validation (pages/posts/create.vue)
<template>
  <div class="max-w-2xl mx-auto">
    <h1 class="text-3xl font-bold mb-8">Create New Post</h1>

    <UForm :state="form" :validate="validate" @submit="onSubmit" class="space-y-6">
      <UFormGroup label="Title" name="title" required>
        <UInput v-model="form.title" placeholder="Enter post title" />
      </UFormGroup>

      <UFormGroup label="Excerpt" name="excerpt">
        <UTextarea v-model="form.excerpt" :rows="3" placeholder="Brief description" />
      </UFormGroup>

      <UFormGroup label="Content" name="content" required>
        <ClientOnly>
          <TiptapEditor v-model="form.content" />
        </ClientOnly>
      </UFormGroup>

      <UFormGroup label="Author" name="author" required>
        <UInput v-model="form.author" placeholder="Author name" />
      </UFormGroup>

      <UFormGroup label="Featured Image" name="featuredImage">
        <UInput v-model="form.featuredImage" placeholder="Image URL" />
      </UFormGroup>

      <div class="flex justify-end space-x-4">
        <UButton to="/posts" variant="outline">
          Cancel
        </UButton>
        <UButton type="submit" :loading="pending">
          Create Post
        </UButton>
      </div>
    </UForm>
  </div>
</template>

<script setup>
const router = useRouter()
const toast = useToast()

// Form state
const form = reactive({
  title: '',
  excerpt: '',
  content: '',
  author: '',
  featuredImage: ''
})

const pending = ref(false)

// Validation
const validate = (state: typeof form) => {
  const errors = []

  if (!state.title) errors.push({ path: 'title', message: 'Title is required' })
  if (!state.content) errors.push({ path: 'content', message: 'Content is required' })
  if (!state.author) errors.push({ path: 'author', message: 'Author is required' })

  return errors
}

// Submit handler
const onSubmit = async (event: FormSubmitEvent<typeof form>) => {
  pending.value = true

  try {
    await $fetch('/api/posts', {
      method: 'POST',
      body: event.data
    })

    toast.add({
      title: 'Success',
      description: 'Post created successfully',
      color: 'green'
    })

    await router.push('/posts')
  } catch (error) {
    toast.add({
      title: 'Error',
      description: 'Failed to create post',
      color: 'red'
    })
  } finally {
    pending.value = false
  }
}

definePageMeta({
  title: 'Create Post',
  description: 'Create a new blog post'
})
</script>

// 8. Composables (composables/usePosts.ts)
export const usePosts = () => {
  const posts = ref<Post[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)

  const fetchPosts = async () => {
    loading.value = true
    error.value = null

    try {
      const data = await $fetch<Post[]>('/api/posts')
      posts.value = data
    } catch (err) {
      error.value = 'Failed to fetch posts'
      console.error(err)
    } finally {
      loading.value = false
    }
  }

  const createPost = async (postData: CreatePostData) => {
    try {
      const newPost = await $fetch<Post>('/api/posts', {
        method: 'POST',
        body: postData
      })
      posts.value.unshift(newPost)
      return newPost
    } catch (err) {
      error.value = 'Failed to create post'
      throw err
    }
  }

  return {
    posts: readonly(posts),
    loading: readonly(loading),
    error: readonly(error),
    fetchPosts,
    createPost
  }
}

// 9. Server API (server/api/posts.post.ts)
export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  // Validate input
  const postSchema = z.object({
    title: z.string().min(1),
    excerpt: z.string().optional(),
    content: z.string().min(1),
    author: z.string().min(1),
    featuredImage: z.string().url().optional()
  })

  try {
    const validatedData = postSchema.parse(body)

    // Create post (in real app, save to database)
    const newPost = {
      id: Date.now().toString(),
      ...validatedData,
      createdAt: new Date().toISOString().split('T')[0]
    }

    return newPost
  } catch (err) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Invalid post data'
    })
  }
})

// 10. Plugin (plugins/api.client.ts)
export default defineNuxtPlugin(() => {
  const api = $fetch.create({
    baseURL: '/api',
    onRequestError({ error }) {
      console.error('API request error:', error)
    },
    onResponseError({ response }) {
      console.error('API response error:', response.status)
    }
  })

  return {
    provide: {
      api
    }
  }
})

// 11. Middleware (middleware/auth.ts)
export default defineNuxtRouteMiddleware((to, from) => {
  const { $auth } = useNuxtApp()

  if (!$auth.isLoggedIn) {
    return navigateTo('/login')
  }
})

// 12. Types (types/index.ts)
export interface Post {
  id: string
  title: string
  excerpt?: string
  content: string
  author: string
  authorAvatar?: string
  createdAt: string
  featuredImage?: string
}

export interface CreatePostData {
  title: string
  excerpt?: string
  content: string
  author: string
  featuredImage?: string
}

💻 Características Avanzadas de Nuxt typescript

🟡 intermediate ⭐⭐⭐⭐

Patrones complejos de Nuxt incluyendo autenticación, caché, optimización SEO y despliegue

⏱️ 50 min 🏷️ nuxt, advanced, full-stack, production
Prerequisites: Nuxt basics, Vue.js, TypeScript, Node.js, Database concepts
// Advanced Nuxt 3 Features

// 1. Authentication with Supabase
// server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
  const { $supabase } = useNuxtApp(event)
  const body = await readBody(event)

  const { data, error } = await $supabase.auth.signInWithPassword({
    email: body.email,
    password: body.password
  })

  if (error) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Invalid credentials'
    })
  }

  // Set session cookie
  setCookie(event, 'auth-token', data.session.access_token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7 // 7 days
  })

  return { user: data.user }
})

// 2. Advanced SEO and Meta Management
// pages/blog/[slug].vue
<script setup>
const route = useRoute()
const { data: post } = await useFetch(`/api/blog/${route.params.slug}`)

// Dynamic meta tags
useSeoMeta({
  title: () => post.value?.title || 'Blog Post',
  description: () => post.value?.excerpt || 'Read our latest blog post',
  ogTitle: () => post.value?.title,
  ogDescription: () => post.value?.excerpt,
  ogImage: () => post.value?.featuredImage || '/og-default.jpg',
  twitterCard: 'summary_large_image',
  twitterTitle: () => post.value?.title,
  twitterDescription: () => post.value?.excerpt,
  twitterImage: () => post.value?.featuredImage || '/og-default.jpg',
  author: () => post.value?.author,
  publishedTime: () => post.value?.createdAt,
  articleSection: 'Technology'
})

// Structured data (JSON-LD)
useHead({
  script: [
    {
      type: 'application/ld+json',
      children: JSON.stringify({
        '@context': 'https://schema.org',
        '@type': 'BlogPosting',
        headline: post.value?.title,
        description: post.value?.excerpt,
        image: post.value?.featuredImage,
        author: {
          '@type': 'Person',
          name: post.value?.author
        },
        datePublished: post.value?.createdAt,
        dateModified: post.value?.updatedAt
      })
    }
  ]
})
</script>

// 3. Caching Strategies
// server/api/posts/[id].ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')

  // Try to get from cache first
  const cached = await useStorage('redis').getItem(`post:${id}`)
  if (cached) {
    return cached
  }

  // Fetch from database
  const post = await getPostFromDatabase(id)

  // Cache for 1 hour
  await useStorage('redis').setItem(`post:${id}`, post, {
    ttl: 60 * 60 // 1 hour in seconds
  })

  // Set cache headers
  setHeader(event, 'Cache-Control', 'public, max-age=3600, s-maxage=3600')

  return post
})

// 4. Real-time Updates with Server-Sent Events
// server/api/sse/posts.ts
export default defineEventHandler(async (event) => {
  const stream = new ReadableStream({
    start(controller) {
      const interval = setInterval(async () => {
        const posts = await getLatestPosts()
        controller.enqueue(`data: ${JSON.stringify(posts)}\n\n`)
      }, 1000)

      // Clean up on disconnect
      event.node.req.on('close', () => {
        clearInterval(interval)
        controller.close()
      })
    }
  })

  return stream
})

// 5. Image Optimization and CDN
// composables/useImages.ts
export const useImages = () => {
  const config = useRuntimeConfig()

  const optimizeImage = (src: string, options?: {
    width?: number
    height?: number
    quality?: number
    format?: 'webp' | 'avif' | 'jpg' | 'png'
  }) => {
    const params = new URLSearchParams()

    if (options?.width) params.set('w', options.width.toString())
    if (options?.height) params.set('h', options.height.toString())
    if (options?.quality) params.set('q', options.quality.toString())
    if (options?.format) params.set('f', options.format)

    const queryString = params.toString()
    const baseUrl = config.public.imageCdn || '/images'

    return queryString ? `${baseUrl}/${src}?${queryString}` : `${baseUrl}/${src}`
  }

  const generateSrcSet = (src: string, widths: number[]) => {
    return widths.map(width =>
      `${optimizeImage(src, { width })} ${width}w`
    ).join(', ')
  }

  return {
    optimizeImage,
    generateSrcSet
  }
}

// 6. Advanced State Management with Pinia
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const loading = ref(false)

  const login = async (credentials: LoginCredentials) => {
    loading.value = true
    try {
      const response = await $fetch('/api/auth/login', {
        method: 'POST',
        body: credentials
      })
      user.value = response.user
    } finally {
      loading.value = false
    }
  }

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

  const fetchUser = async () => {
    try {
      const response = await $fetch('/api/auth/me')
      user.value = response.user
    } catch {
      user.value = null
    }
  }

  return {
    user: readonly(user),
    loading: readonly(loading),
    login,
    logout,
    fetchUser
  }
})

// 7. Progressive Web App (PWA) Configuration
// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@vite-pwa/nuxt',
    '@nuxt/image'
  ],
  pwa: {
    registerType: 'autoUpdate',
    workbox: {
      navigateFallback: '/',
      globPatterns: ['**/*.{js,css,html,png,svg,ico}']
    },
    client: {
      installPrompt: true
    },
    manifest: {
      name: 'My Nuxt App',
      short_name: 'NuxtApp',
      description: 'A modern Nuxt 3 application',
      theme_color: '#ffffff',
      background_color: '#ffffff',
      display: 'standalone',
      orientation: 'portrait',
      scope: '/',
      start_url: '/',
      icons: [
        {
          src: 'pwa-192x192.png',
          sizes: '192x192',
          type: 'image/png'
        },
        {
          src: 'pwa-512x512.png',
          sizes: '512x512',
          type: 'image/png'
        }
      ]
    }
  }
})

// 8. Performance Monitoring
// plugins/analytics.client.ts
export default defineNuxtPlugin((nuxtApp) => {
  if (process.env.NODE_ENV === 'production') {
    // Google Analytics
    useHead({
      script: [
        {
          async: true,
          src: `https://www.googletagmanager.com/gtag/js?id=${process.env.GA_ID}`
        },
        {
          innerHTML: `
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', '${process.env.GA_ID}');
          `
        }
      ]
    })

    // Sentry for error tracking
    const { $sentry } = nuxtApp.vueApp.config.globalProperties
    if ($sentry) {
      $sentry.init({
        dsn: process.env.SENTRY_DSN,
        environment: process.env.NODE_ENV
      })
    }
  }
})

// 9. Multi-language Support (i18n)
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/i18n'],
  i18n: {
    locales: [
      {
        code: 'en',
        name: 'English',
        file: 'en.json'
      },
      {
        code: 'es',
        name: 'Español',
        file: 'es.json'
      },
      {
        code: 'fr',
        name: 'Français',
        file: 'fr.json'
      }
    ],
    defaultLocale: 'en',
    langDir: 'locales',
    lazy: true,
    detectBrowserLanguage: {
      useCookie: true,
      cookieKey: 'i18n_redirected',
      redirectOn: 'root'
    }
  }
})

// 10. Deployment Configuration
// .env.production
NUXT_PUBLIC_API_BASE=https://api.myapp.com
NUXT_PUBLIC_SITE_URL=https://myapp.com
NUXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NUXT_PUBLIC_SENTRY_DSN=https://[email protected]/xxx

// Database
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp

// Authentication
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_ANON_KEY=xxx

// Redis
REDIS_URL=redis://localhost:6379

// 11. Docker Configuration
// Dockerfile
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:18-alpine AS runner

WORKDIR /app

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nuxt

COPY --from=builder /app/.output ./.output
COPY --from=builder /app/package.json ./package.json

USER nuxt

EXPOSE 3000

ENV NUXT_HOST=0.0.0.0
ENV PORT=3000
ENV NODE_ENV=production

CMD ["node", ".output/server/index.mjs"]

// docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis

  db:
    image: postgres:15
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

// 12. Testing with Vitest and Playwright
// tests/components/Button.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '~/components/Button.vue'

describe('Button', () => {
  it('renders correctly', () => {
    const wrapper = mount(Button, {
      props: { text: 'Click me' }
    })

    expect(wrapper.text()).toContain('Click me')
    expect(wrapper.find('button').exists()).toBe(true)
  })

  it('emits click event', async () => {
    const wrapper = mount(Button, {
      props: { text: 'Click me' }
    })

    await wrapper.find('button').trigger('click')

    expect(wrapper.emitted('click')).toBeTruthy()
  })
})

// tests/e2e/blog.spec.ts
import { test, expect } from '@playwright/test'

test('blog page loads correctly', async ({ page }) => {
  await page.goto('/blog')

  await expect(page.locator('h1')).toContainText('Blog')
  await expect(page.locator('[data-testid="post-list"]').isVisible()).toBe(true()
})

test('can navigate to post detail', async ({ page }) => {
  await page.goto('/blog')

  await page.locator('[data-testid="post-link"]').first().click()

  await expect(page.locator('h1')).toBeVisible()
  await expect(page.locator('[data-testid="post-content"]')).toBeVisible()
})