Nuxt.js é o meta-framework de Vue.js que disponibiliza server-side rendering (SSR), static site generation (SSG) e um poderoso sistema de routing baseado em ficheiros. Simplifica a criação de aplicações Vue com bom desempenho e SEO. Este guia aborda o desenvolvimento com Nuxt 3 na perspetiva de um developer sénior.
Porquê Nuxt.js
O Nuxt oferece vantagens significativas:
- SEO-Friendly: Server-side rendering para motores de pesquisa
- Performance: Code splitting e otimização automáticos
- Routing baseado em ficheiros: Não é necessária configuração do router
- Auto-Imports: Componentes e composables disponíveis em todo o lado
- Full-Stack: Rotas de API e middleware de servidor incluídos
Primeiros passos
Criar projeto
npx nuxi@latest init my-app
cd my-app
npm install
npm run dev
Estrutura do projeto
my-app/
├── .nuxt/ # Ficheiros gerados (gitignore)
├── assets/ # Assets não compilados (Sass, imagens)
├── components/ # Componentes Vue (autoimportados)
├── composables/ # Composables Vue (autoimportados)
├── layouts/ # Layouts de página
├── middleware/ # Middleware de rota
├── pages/ # Rotas baseadas em ficheiros
├── plugins/ # Plugins Vue
├── public/ # Ficheiros estáticos (servidos tal como estão)
├── server/
│ ├── api/ # Rotas de API
│ └── middleware/ # Middleware de servidor
├── nuxt.config.ts # Configuração do Nuxt
└── app.vue # Componente raiz
Routing baseado em ficheiros
Rotas básicas
Crie ficheiros no diretório pages/:
pages/
├── index.vue # /
├── about.vue # /about
├── contact.vue # /contact
└── users/
├── index.vue # /users
└── [id].vue # /users/:id (dinâmico)
Rotas dinâmicas
pages/users/[id].vue:
<script setup>
const route = useRoute()
const userId = route.params.id
// Fetch user data
const { data: user } = await useFetch(`/api/users/${userId}`)
</script>
<template>
<div>
<h1>{{ user?.name }}</h1>
<p>{{ user?.email }}</p>
</div>
</template>
Rotas aninhadas
pages/
└── users/
├── index.vue # /users
├── [id].vue # /users/:id
└── [id]/
├── posts.vue # /users/:id/posts
└── settings.vue # /users/:id/settings
Obtenção de dados
Composable useFetch
<script setup>
// Basic fetch
const { data, pending, error, refresh } = await useFetch('/api/posts')
// With options
const { data: posts } = await useFetch('/api/posts', {
query: { limit: 10, page: 1 },
headers: { 'Authorization': 'Bearer token' },
transform: (response) => response.data,
})
// Lazy loading (doesn't block navigation)
const { data: comments, pending } = useLazyFetch('/api/comments')
// Watch for changes and refetch
const page = ref(1)
const { data } = await useFetch('/api/posts', {
query: { page },
watch: [page],
})
</script>
<template>
<div>
<div v-if="pending">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else>
<li v-for="post in posts" :key="post.id">
{{ post.title }}
</li>
</ul>
</div>
</template>
useAsyncData para lógica personalizada
<script setup>
const { data: user } = await useAsyncData('user', async () => {
// Custom async logic
const profile = await $fetch('/api/profile')
const settings = await $fetch('/api/settings')
return {
...profile,
settings,
}
})
</script>
Gestão de estado com Pinia
Instalar e configurar
npm install @pinia/nuxt
Adicionar a nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
})
Criar store
stores/counter.ts:
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
// Estado
const count = ref(0)
// Getters
const doubleCount = computed(() => count.value * 2)
// Actions
function increment() {
count.value++
}
async function fetchCount() {
const { data } = await useFetch('/api/count')
count.value = data.value
}
return { count, doubleCount, increment, fetchCount }
})
Usar store no componente
<script setup>
const counter = useCounterStore()
</script>
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">+1</button>
</div>
</template>
Rotas de servidor (API)
Criar endpoint de API
server/api/users/index.get.ts:
export default defineEventHandler(async (event) => {
// Aceder a parâmetros de query
const query = getQuery(event)
const { page = 1, limit = 10 } = query
// Obter a partir da base de dados ou de uma API externa
const users = await fetchUsers({ page, limit })
return {
data: users,
meta: { page, limit },
}
})
server/api/users/[id].get.ts:
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const user = await fetchUser(id)
if (!user) {
throw createError({
statusCode: 404,
message: 'User not found',
})
}
return user
})
Handler de pedido POST
server/api/users/index.post.ts:
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validar
if (!body.email || !body.name) {
throw createError({
statusCode: 400,
message: 'Name and email required',
})
}
// Criar utilizador
const user = await createUser(body)
setResponseStatus(event, 201)
return user
})
Layouts
Layout por omissão
layouts/default.vue:
<template>
<div class="layout">
<header>
<nav>
<NuxtLink to="/">Home</NuxtLink>
<NuxtLink to="/about">About</NuxtLink>
</nav>
</header>
<main>
<slot />
</main>
<footer>
© {{ new Date().getFullYear() }}
</footer>
</div>
</template>
<style scoped>
.layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
</style>
Layout personalizado
layouts/admin.vue:
<template>
<div class="admin-layout">
<aside class="sidebar">
<!-- Admin navigation -->
</aside>
<main class="content">
<slot />
</main>
</div>
</template>
Usar na página:
<script setup>
definePageMeta({
layout: 'admin',
})
</script>
Middleware
Middleware de rota
middleware/auth.ts:
export default defineNuxtRouteMiddleware((to, from) => {
const user = useAuthUser()
if (!user.value) {
return navigateTo('/login')
}
})
Aplicar à página:
<script setup>
definePageMeta({
middleware: ['auth'],
})
</script>
Middleware global
middleware/analytics.global.ts:
export default defineNuxtRouteMiddleware((to, from) => {
// Registar visualização de página
trackPageView(to.path)
})
SEO e meta
Meta da página
<script setup>
useHead({
title: 'My Page Title',
meta: [
{ name: 'description', content: 'Page description for SEO' },
{ property: 'og:title', content: 'My Page Title' },
{ property: 'og:image', content: '/og-image.png' },
],
link: [
{ rel: 'canonical', href: 'https://example.com/page' },
],
})
</script>
Meta dinâmico
<script setup>
const { data: post } = await useFetch(`/api/posts/${route.params.id}`)
useHead({
title: () => post.value?.title || 'Loading...',
meta: [
{ name: 'description', content: () => post.value?.excerpt },
],
})
</script>
Configuração de ambiente
Runtime config
nuxt.config.ts:
export default defineNuxtConfig({
runtimeConfig: {
// Chaves privadas (apenas no servidor)
apiSecret: process.env.API_SECRET,
// Chaves públicas (expostas ao cliente)
public: {
apiBase: process.env.API_BASE_URL || '/api',
},
},
})
Aceder no código:
<script setup>
const config = useRuntimeConfig()
// Server-side only
// config.apiSecret
// Client and server
const apiBase = config.public.apiBase
</script>
Deployment
Geração estática
npm run generate
Gera HTML estático em .output/public/.
Deployment SSR
npm run build
Faça deployment do diretório .output/. Para Node.js:
node .output/server/index.mjs
Serverless (AWS Lambda)
nuxt.config.ts:
export default defineNuxtConfig({
nitro: {
preset: 'aws-lambda',
},
})
Build e deployment:
npm run build
# Fazer deployment de .output/ para AWS Lambda
Principais conclusões
- Routing baseado em ficheiros: Convenção em vez de configuração
- Auto-imports: Menos boilerplate, mais produtividade
- useFetch para dados: Caching integrado e suporte SSR
- Rotas de servidor: Full-stack num único projeto
- Pinia para estado: Gestão de estado simples e type-safe
- Deployment flexível: SSR, SSG ou híbrido
O Nuxt 3 simplifica drasticamente o desenvolvimento em Vue.js, ao mesmo tempo que disponibiliza funcionalidades de nível empresarial para SEO, performance e capacidades full-stack.