Manual Completo de Nuxt.js con TypeScript

Introducción a Nuxt.js con TypeScript

Nuxt.js es un framework de Vue.js para crear aplicaciones universales, estáticas y de servidor (SSR). Con TypeScript, obtienes un sistema de tipos robusto para tu aplicación.

Beneficios de Nuxt.js + TypeScript:

Crear un proyecto Nuxt.js con TypeScript

# Crear nuevo proyecto
npx create-nuxt-app mi-app-nuxt

# Durante la creación, seleccionar TypeScript
? Programming language: TypeScript
? Will you use TypeScript or JavaScript? TypeScript

# Estructura básica del proyecto
.
├── components/
├── layouts/
├── pages/
├── plugins/
├── static/
├── store/
├── nuxt.config.ts      # Configuración de Nuxt
├── tsconfig.json       # Configuración de TypeScript
└── package.json

Configuración Inicial

nuxt.config.ts

import { NuxtConfig } from '@nuxt/types'

const config: NuxtConfig = {
  // Mode: 'universal' o 'spa'
  ssr: true,
  
  // Configuración de TypeScript
  typescript: {
    typeCheck: {
      eslint: true
    }
  },

  // Módulos
  modules: [
    '@nuxtjs/axios',
    '@nuxtjs/auth-next'
  ],

  // Configuración de Axios
  axios: {
    baseURL: process.env.API_URL || 'https://api.example.com'
  },

  // Configuración de compilación
  build: {
    transpile: ['vee-validate/dist/rules'],
    loaders: {
      scss: { additionalData: `@import "~/assets/scss/variables.scss";` }
    }
  },

  // Variables de entorno
  publicRuntimeConfig: {
    appName: process.env.APP_NAME || 'Mi App Nuxt'
  }
}

export default config

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "ESNext",
    "moduleResolution": "Node",
    "lib": ["ESNext", "ESNext.AsyncIterable", "DOM"],
    "esModuleInterop": true,
    "allowJs": true,
    "sourceMap": true,
    "strict": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "~/*": ["./*"],
      "@/*": ["./*"]
    },
    "types": ["@types/node", "@nuxt/types"]
  },
  "exclude": ["node_modules"]
}

Componentes con TypeScript

Options API

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component
export default class MyComponent extends Vue {
  // Propiedades reactivas
  private counter: number = 0
  private message: string = 'Hola Nuxt!'

  // Props
  @Prop({ type: String, required: true }) readonly title!: string
  @Prop({ default: 30 }) readonly age!: number

  // Computed
  get reversedMessage(): string {
    return this.message.split('').reverse().join('')
  }

  // Métodos
  increment(): void {
    this.counter++
  }

  // Hooks del ciclo de vida
  mounted(): void {
    console.log('Componente montado')
  }
}
</script>

Composition API

<script lang="ts">
import { defineComponent, ref, computed, onMounted } from '@nuxtjs/composition-api'

export default defineComponent({
  props: {
    title: {
      type: String,
      required: true
    },
    age: {
      type: Number,
      default: 30
    }
  },
  
  setup(props, { emit }) {
    const counter = ref(0)
    const message = ref('Hola Nuxt!')

    const reversedMessage = computed(() => {
      return message.value.split('').reverse().join('')
    })

    function increment() {
      counter.value++
      emit('incremented', counter.value)
    }

    onMounted(() => {
      console.log('Componente montado')
    })

    return {
      counter,
      message,
      reversedMessage,
      increment
    }
  }
})
</script>

Vuex con TypeScript

Store tipado (Class-based)

// store/modules/user.ts
import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators'
import { User } from '~/types/user'

@Module({
  name: 'user',
  stateFactory: true,
  namespaced: true
})
export default class UserModule extends VuexModule {
  private user: User | null = null
  private token: string | null = null

  get isAuthenticated(): boolean {
    return !!this.token
  }

  get currentUser(): User | null {
    return this.user
  }

  @Mutation
  SET_USER(user: User): void {
    this.user = user
  }

  @Mutation
  SET_TOKEN(token: string): void {
    this.token = token
  }

  @Action
  async login({ email, password }: { email: string; password: string }) {
    const response = await this.$axios.$post('/auth/login', { email, password })
    this.SET_TOKEN(response.token)
    this.SET_USER(response.user)
  }

  @Action
  logout(): void {
    this.SET_TOKEN(null)
    this.SET_USER(null)
  }
}

Store tipado (Composition API)

// composables/useUserStore.ts
import { computed } from '@nuxtjs/composition-api'
import { useStore } from '~/store'

export const useUserStore = () => {
  const store = useStore()

  const isAuthenticated = computed(() => store.state.user.token !== null)
  const currentUser = computed(() => store.state.user.user)

  const login = async (email: string, password: string) => {
    const response = await store.$axios.$post('/auth/login', { email, password })
    store.commit('user/SET_TOKEN', response.token)
    store.commit('user/SET_USER', response.user)
  }

  const logout = () => {
    store.commit('user/SET_TOKEN', null)
    store.commit('user/SET_USER', null)
  }

  return {
    isAuthenticated,
    currentUser,
    login,
    logout
  }
}

Rutas y Middleware

Tipos para rutas

// types/router.d.ts
import { Route } from 'vue-router'

declare module 'vue/types/vue' {
  interface Vue {
    $router: VueRouter
    $route: Route & {
      params: {
        id?: string
        slug?: string
      }
    }
  }
}

// Uso en componentes
this.$route.params.id // Tipado como string | undefined

Middleware tipado

// middleware/auth.ts
import { Context } from '@nuxt/types'

export default function ({ store, redirect }: Context) {
  // Si el usuario no está autenticado
  if (!store.state.user.token) {
    return redirect('/login')
  }
}

// Uso en rutas
<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'

export default defineComponent({
  middleware: 'auth',
  // ...
})
</script>

API y Fetching de Datos

useFetch con TypeScript

// composables/usePosts.ts
import { useAsync, useContext } from '@nuxtjs/composition-api'

interface Post {
  id: number
  title: string
  body: string
  userId: number
}

export const usePosts = () => {
  const { $axios } = useContext()

  const fetchPosts = useAsync(async () => {
    const posts = await $axios.$get<Post[]>('/posts')
    return posts
  })

  return {
    fetchPosts
  }
}

asyncData y fetch

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import { Post } from '~/types'

export default defineComponent({
  async asyncData({ $axios }) {
    const posts = await $axios.$get<Post[]>('/posts')
    return { posts }
  },

  data() {
    return {
      posts: [] as Post[]
    }
  },

  async fetch() {
    this.posts = await this.$axios.$get<Post[]>('/posts')
  }
})
</script>

Plugins y Módulos

Plugins con TypeScript

// plugins/axios-accessor.ts
import { Plugin } from '@nuxt/types'
import { initializeAxios } from '~/utils/api'

const accessor: Plugin = ({ $axios }) => {
  initializeAxios($axios)
}

export default accessor

// types/vue.d.ts
import { AxiosInstance } from 'axios'

declare module 'vue/types/vue' {
  interface Vue {
    $axios: AxiosInstance
  }
}

declare module '@nuxt/types' {
  interface NuxtAppOptions {
    $axios: AxiosInstance
  }
}

Módulos de Nuxt

// nuxt.config.ts
export default {
  modules: [
    ['@nuxtjs/axios', {
      proxy: true,
      credentials: true
    }],
    ['@nuxtjs/auth-next', {
      strategies: {
        local: {
          token: {
            property: 'token',
            required: true,
            type: 'Bearer'
          },
          user: {
            property: 'user',
            autoFetch: true
          },
          endpoints: {
            login: { url: '/auth/login', method: 'post' },
            logout: { url: '/auth/logout', method: 'post' },
            user: { url: '/auth/user', method: 'get' }
          }
        }
      }
    }]
  ]
}

Testing

Jest y Testing Library

// tests/componentes/Button.spec.ts
import { mount } from '@vue/test-utils'
import Button from '~/components/Button.vue'

describe('Button', () => {
  test('emits click event', () => {
    const wrapper = mount(Button, {
      propsData: {
        label: 'Click me'
      }
    })

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

// jest.config.js
module.exports = {
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
    '^~/(.*)$': '<rootDir>/$1'
  },
  moduleFileExtensions: ['js', 'ts', 'json', 'vue'],
  transform: {
    '^.+\\.ts$': 'ts-jest',
    '^.+\\.vue$': 'vue-jest'
  },
  collectCoverage: true,
  collectCoverageFrom: [
    'components/**/*.vue',
    'pages/**/*.vue'
  ]
}

Cypress Component Testing

// tests/e2e/specs/login.spec.ts
describe('Login', () => {
  it('successfully logs in', () => {
    cy.visit('/login')
    cy.get('[data-test="email"]').type('user@example.com')
    cy.get('[data-test="password"]').type('password123')
    cy.get('[data-test="submit"]').click()
    cy.url().should('include', '/dashboard')
  })
})

Despliegue y Optimización

Generación estática (SSG)

// nuxt.config.ts
export default {
  target: 'static',
  generate: {
    routes: [
      '/about',
      '/contact',
      async () => {
        const posts = await $axios.$get('/posts')
        return posts.map(post => `/posts/${post.id}`)
      }
    ]
  }
}

// Generar la aplicación
npm run generate

Despliegue en Vercel

  1. Conectar tu repositorio GitHub/GitLab/Bitbucket
  2. Seleccionar el proyecto Nuxt.js
  3. Configurar variables de entorno si es necesario
  4. Deploy automático con cada push

Conclusión

Nuxt.js con TypeScript ofrece una combinación poderosa para construir aplicaciones Vue.js universales, estáticas o de servidor con un sistema de tipos robusto.

Recursos adicionales: