Nuxt 4
Nuxt es un framework gratuito y de código abierto que se basa en Vue.js y está diseñado para facilitar el desarrollo de aplicaciones web intuitivas, rápidas y de calidad profesional. Con Nuxt, puedes empezar a escribir archivos .vue desde el principio y disfrutar de funcionalidades como recarga en caliente durante el desarrollo y renderizado en el servidor (SSR) por defecto en producción, lo que mejora el rendimiento y la experiencia del usuario Introducción Nuxt 4.x.
¿Qué beneficios tiene Nuxt sobre trabajar solo con Vue?
- Renderizado en el servidor (SSR) por defecto: Nuxt puede generar páginas HTML en el servidor antes de enviarlas al navegador, lo que mejora el SEO, la velocidad de carga inicial y la accesibilidad. Con Vue puro, esto requiere mucha configuración manual SSR en Nuxt 4.x.
- Enrutamiento automático: Nuxt crea rutas automáticamente a partir de la estructura de carpetas, evitando la configuración manual de rutas que sí necesitas en Vue puro Introducción Nuxt 4.x.
- División automática de código: Nuxt divide tu código en partes más pequeñas para que la aplicación cargue más rápido.
- Auto-importación de componentes y composables: No necesitas importar manualmente los componentes o funciones reutilizables, Nuxt lo hace por ti y solo incluye lo que usas en el bundle final.
- Soporte para TypeScript sin configuración: Puedes escribir código seguro y tipado sin preocuparte por la configuración.
- Herramientas de desarrollo integradas: Usa Vite por defecto para un desarrollo más rápido y eficiente.
- Flexibilidad de despliegue: Puedes desplegar tu app en servidores tradicionales, serverless, edge o como sitio estático sin cambios en el código.
En resumen, Nuxt automatiza y simplifica muchas tareas repetitivas y complejas que tendrías que hacer manualmente con Vue puro, permitiéndote enfocarte en crear tu aplicación Introducción Nuxt 4.x.
Instalación
- Tener instalado Node.js (v.18.0.0 en adelante)
npm create nuxt@latest <project-name>Extensiones VSCode
app.vue
De forma predeterminada, Nuxt tratará este archivo como punto de entrada y mostrará su contenido para cada ruta de la aplicación.
TIP
Si está familiarizado con Vue, quizás se pregunte dónde está main.js (el archivo que normalmente crea una aplicación Vue). Nuxt hace esto detrás de escena.
Componentes proporcionados por Nuxt
Los componentes NuxtRouteAnnouncer y NuxtWelcome son componentes proporcionados directamente por Nuxt:
NuxtRouteAnnouncer: Este componente es parte del núcleo de Nuxt (disponible desde Nuxt v3.12+). Su función es mejorar la accesibilidad anunciando los cambios de ruta a tecnologías de asistencia, como los lectores de pantalla. Puedes usarlo simplemente agregándolo en tu archivo
app.vueo en tus layouts. No necesitas instalar nada adicional, ya que viene incluido con Nuxt a partir de la versión mencionada. Más detalles y ejemplos de uso se encuentran en la documentación oficial de Nuxt 3 y 4 NuxtRouteAnnouncer - Nuxt 4.x NuxtRouteAnnouncer - Nuxt 3.x.NuxtWelcome: Este componente también es proporcionado por Nuxt y está pensado para dar la bienvenida en proyectos nuevos creados a partir del template inicial de Nuxt. Incluye enlaces útiles a la documentación y recursos de la comunidad. Se puede usar directamente en el archivo principal de la aplicación (
app.vueoapp/app.vue), y forma parte del paquetenuxt/assetsNuxtWelcome - Nuxt 4.x NuxtWelcome - Nuxt 3.x.
app/pages
Las páginas representan vistas para cada patrón de ruta específico. Cada archivo en el directorio representa una ruta diferente que muestra su contenido.
/pages/index.vue
<template>
<h1>Home page</h1>
</template>/pages/about.vue
<template>
<h1>About page</h1>
</template>app.vue (se puede eliminar si no lo necesita)
<template>
<NuxtPage />
</template>app/components
La mayoría de los componentes son piezas reutilizables de la interfaz de usuario, como botones y menús. En Nuxt, puede crear estos componentes en el directorio app/components y estarán disponibles automáticamente en su aplicación sin tener que importarlos explícitamente. más información sobre componentes
Los nombres de los archivos de componentes deben usar PascalCase o kebab-case.
PascalCase
| components/
--| AppHeader.vue
--| AppFooter.vuekebab-case
| components/
--| app-header.vue
--| app-footer.vueapp.vue
<template>
<div>
<AppHeader />
<NuxtPage />
<AppFooter />
</div>
</template>Subdirectorios también son compatibles
| components/
--| AppHeader.vue
--| AppFooter.vue
--| ui/
----| Button.vue
--| card/
----| CardHeader.vue<template>
<h1>Home page</h1>
<UiButton />
<CardHeader />
</template>Componentes con Props
app\components\title-page.vue
<script setup lang="ts">
interface Props {
title: string;
}
const { title } = defineProps<Props>();
</script>
<template>
<h1>{{ title }}</h1>
</template>app\pages\index.vue
<template>
<TitlePage title="Home" />
</template>app\pages\about.vue
<template>
<TitlePage title="About" />
</template>PascalCase vs kebab-case (recomendación de Nuxt)
Nuxt recomienda utilizar el formato PascalCase para nombrar y llamar a los componentes auto-importados. Por ejemplo, si tienes un componente en una ruta anidada como components/base/foo/Button.vue, el nombre del componente será <BaseFooButton /> y así es como deberías usarlo en tus plantillas. Esto ayuda a mantener claridad y coherencia, especialmente cuando los componentes están en subdirectorios components - Nuxt 4.x components - Nuxt 3.x.
Aunque Vue permite usar tanto PascalCase como kebab-case en los templates, la documentación de Nuxt recomienda que el nombre del archivo coincida con el nombre del componente en PascalCase y que se use ese mismo formato al importarlo o referenciarlo en el template. Esto es especialmente importante para evitar confusiones con componentes auto-importados desde rutas anidadas.
For clarity, we recommend that the component's filename matches its name. So, in the example above, you could rename
Button.vueto beBaseFooButton.vue.
...the component's name will be:<BaseFooButton />
components - Nuxt 4.x
Slots
La razón por la que en la documentación de Nuxt aparece el ejemplo de <slot /> y no <Slot /> es porque <slot /> es un componente nativo de Vue, no un componente personalizado. En Vue (y por extensión en Nuxt), los elementos nativos como <slot>, <template>, y <component> siempre se escriben en minúsculas, siguiendo la convención de HTML. Esto es diferente a los componentes personalizados, que se recomienda escribir en PascalCase, como <MyComponent /> Slot - Nuxt Content.
Por lo tanto, cuando ves <slot /> en la documentación, es porque se está refiriendo al slot nativo de Vue, no a un componente auto-importado o personalizado.
app/layouts
Los diseños son envoltorios de páginas que contienen una interfaz de usuario común para varias páginas, como un encabezado y un pie de página. más información sobre layouts
app/layouts/default.vue
<template>
<div>
<header>
<nav>
<NuxtLink to="/">Home</NuxtLink> |
<NuxtLink to="/about">About</NuxtLink>
</nav>
</header>
<slot />
<footer>
<p>© 2024 My Nuxt Application</p>
</footer>
</div>
</template>Los layouts se habilitan añadiendo el componente <NuxtLayout> en el archivo app.vue.
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>SEO y metadata
Existen varias formas de agregar metadatos a nuestra aplicación. veremos dos de las más utilizadas. más información sobre SEO y metadata
useHead
<script setup lang="ts">
useHead({
title: "My App",
meta: [{ name: "description", content: "My amazing site." }],
link: [
{
rel: "preconnect",
href: "https://fonts.googleapis.com",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Roboto&display=swap",
crossorigin: "",
},
],
});
</script>useSeoMeta
<script setup lang="ts">
useSeoMeta({
title: "My Amazing Site",
ogTitle: "My Amazing Site",
description: "This is my amazing site, let me tell you all about it.",
ogDescription: "This is my amazing site, let me tell you all about it.",
ogImage: "https://example.com/image.png",
twitterCard: "summary_large_image",
});
</script>TIP
El ogTitle es el título que se mostrará cuando compartas la URL de tu sitio en una plataforma de redes sociales que admita etiquetas Open Graph, como Facebook. Este título se utilizará en la vista previa de la publicación compartida, y es diferente del título de la página web en sí. Puede ser más descriptivo o adaptado a la plataforma de redes sociales en la que se comparte el enlace.
nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
app: {
head: {
title: "Nuxt.js TypeScript project",
link: [
{
rel: "stylesheet",
href: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css",
integrity:
"sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN",
crossorigin: "anonymous",
},
],
},
},
});Pages
Cómo vimos anteriormente solo basta con crear un archivo en la carpeta pages para que se cree una ruta en nuestra aplicación. El enrutamiento de Nuxt se basa en vue-router.
| pages/
---| about.vue
---| index.vue
---| posts/
-----| [id].vueNuxtLink
La ventaja de usar <NuxtLink> en lugar de <a> es que Nuxt manejará la navegación del lado del cliente, lo que significa que no se recargará toda la página al hacer clic en un enlace, proporcionando una experiencia de usuario más fluida y rápida.
components/AppHeader.vue
<template>
<header>
<nav>
<ul>
<li><NuxtLink to="/about">About</NuxtLink></li>
<li><NuxtLink to="/posts/1">Post 1</NuxtLink></li>
<li><NuxtLink to="/posts/2">Post 2</NuxtLink></li>
</ul>
</nav>
</header>
</template>Parámetros de ruta
Se utiliza para acceder a la información de la ruta actual. Por ejemplo, si queremos mostrar el id de la ruta posts/[id].vue
/pages/posts/[id].vue
<script setup lang="ts">
const route = useRoute();
// When accessing /posts/1, route.params.id will be 1
console.log(route.params.id);
</script>Parámetros opcionales
Si queremos que un parámetro de ruta sea opcional, podemos agregar doble corchetes alrededor del nombre del parámetro.
/pages/posts/[[id]].vue
Parámetros dinámicos anidados
Si queremos capturar rutas anidadas, podemos usar tres puntos suspensivos antes del nombre del parámetro.
/pages/posts/[...id].vue
useFetch vs $fetch
useFetches el composable preferido para cualquier solicitud de datos que se ejecute durante la fase de configuración (setup) de un componente o página, ya que garantiza que la lógica de renderizado universal (evitar la doble obtención de datos en SSR/CSR) se maneje correctamente.useFetch(url)es casi equivalente auseAsyncData(url, () => event.$fetch(url)).$fetches la utilidad de bajo nivel para realizar solicitudes HTTP puras. Es ideal para llamadas de API que se disparan en respuesta a interacciones del usuario o que se ejecutan exclusivamente en el lado del cliente (donde no se necesita la transferencia de datos entre servidor y cliente)
useFetch
Obtenga datos de un punto final de API con un elemento componible compatible con SSR. más información sobre useFetch
pages\posts\index.vue
<script setup lang="ts">
import type { Post } from "@/interfaces/Post";
const { data: posts } = await useFetch<Post[]>(
"https://jsonplaceholder.typicode.com/posts"
);
</script>
<template>
<div>
<h1>Lista de Posts</h1>
<CardMain
v-for="{ title, body, id } of posts"
:title="title"
:body="body"
:id="id"
/>
</div>
</template>interfaces\Post.ts
export interface Post {
title: string;
body: string;
id: number;
userId: number;
}components\card\CardMain.vue
<script setup lang="ts">
interface Props {
title: string;
body: string;
id: number;
}
const { title, body, id } = defineProps<Props>();
</script>
<template>
<article>
<h2>{{ title }}</h2>
<p>{{ body }}</p>
<NuxtLink :to="`/posts/${id}`">
<span>Read more</span>
</NuxtLink>
</article>
</template>pages\posts[id].vue
<script setup lang="ts">
import type { Post } from "@/interfaces/Post";
const { id } = useRoute().params;
const { data: post } = await useFetch<Post>(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
</script>
<template>
<div>
<h1>{{ post?.id }} - {{ post?.title }}</h1>
<p>{{ post?.body }}</p>
</div>
</template>server
Nuxt escanea automáticamente los archivos dentro de estos directorios para registrar una API.
Los nombres de los archivos de manejo pueden tener el sufijo .get, .post, .put, .delete, ... para que coincida con el método HTTP de la solicitud . Por ejemplo, posts.get.ts se manejará para las solicitudes GET a /api/posts. más información sobre server
Estructura de directorios
-| server/
---| api/
-----| hello.ts # /api/hello
---| routes/
-----| bonjour.ts # /bonjour
---| middleware/
-----| log.ts # log all requestsserver\api\posts.get.ts
export default defineEventHandler((event) => {
const posts = [
{
userId: 1,
id: 1,
title:
"sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto",
},
{
userId: 1,
id: 2,
title: "qui est esse",
body: "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla",
},
{
userId: 1,
id: 3,
title: "ea molestias quasi exercitationem repellat qui ipsa sit aut",
body: "et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut",
},
];
return posts;
});pages\posts\index.vue
<script setup lang="ts">
import type { Post } from "@/interfaces/Post";
const { data: posts } = await useFetch<Post[]>("/api/posts");
</script>
<template>
<div>
<h1>Lista de Posts</h1>
<CardMain
v-for="{ title, body, id } of posts"
:title="title"
:body="body"
:id="id"
/>
</div>
</template>server\api\posts[id].get.ts
import { Post } from "@/interfaces/Post";
export default defineEventHandler(async (event) => {
const { id } = getRouterParams(event);
const post = await $fetch<Post>(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
return post;
});pages\posts[id].vue
<script setup lang="ts">
import type { Post } from "@/interfaces/Post";
const { id } = useRoute().params;
const { data: post } = await useFetch<Post>(`/api/posts/${id}`);
</script>
<template>
<div>
<h1>{{ post?.id }} - {{ post?.title }}</h1>
<p>{{ post?.body }}</p>
</div>
</template>$fetch
Realiza solicitudes HTTP. Es una versión mejorada de la función nativa fetch con soporte para interceptores, reintentos automáticos, serialización/deserialización automática y más. más información sobre $fetch
const data = await $fetch("https://jsonplaceholder.typicode.com/posts");
console.log(data);try {
// Uso de $fetch para enviar la petición POST a la API.
// $fetch es auto-importado globalmente.
const response = await $fetch("/api/login", {
method: "POST", // Especificar el método HTTP
body: {
username: username.value,
password: password.value,
},
// $fetch automáticamente parsea el cuerpo si es un objeto y establece el Content-Type.
});
// Debe existir una propiedad 'success' en la respuesta del servidor.
if (response.success) {
console.log("Login successful, token:", response.token);
// Redireccionar o almacenar el token
// Es importante usar navigateTo para la navegación programática [18].
await navigateTo("/dashboard");
}
} catch (err) {
// Manejo de errores, por ejemplo, si el servidor devuelve 401
loginError.value = err.data?.message || "Login failed.";
console.error("Login failed:", err);
} finally {
loading.value = false;
}Ejemplo de login con NuxtUI
<script setup lang="ts">
definePageMeta({
middleware: "authenticated",
});
import type { NuxtError } from "#app";
import type { LoginSchemaType } from "#shared/zod/login.schema";
import { loginSchema } from "#shared/zod/login.schema";
import type { AuthFormField, FormSubmitEvent } from "@nuxt/ui";
const { user, loggedIn, fetch: refreshSession } = useUserSession();
const toast = useToast();
const serverError = ref<string | undefined>(undefined);
const loading = ref(false);
const fields: AuthFormField[] = [
{
name: "email",
type: "email",
label: "Correo",
placeholder: "Introduce tu correo",
required: true,
defaultValue: "test1@test.com",
},
{
name: "password",
label: "Contraseña",
type: "password",
placeholder: "Introduce tu contraseña",
required: true,
defaultValue: "123123",
},
{
name: "remember",
label: "Recuérdame",
type: "checkbox",
},
];
const providers = [
{
label: "Google",
icon: "i-simple-icons-google",
onClick: () => {
toast.add({ title: "Google", description: "Login with Google" });
},
},
{
label: "GitHub",
icon: "i-simple-icons-github",
onClick: () => {
toast.add({ title: "GitHub", description: "Login with GitHub" });
},
},
];
async function onSubmit(payload: FormSubmitEvent<LoginSchemaType>) {
try {
loading.value = true;
serverError.value = undefined;
await $fetch("/api/login", {
method: "POST",
body: {
email: payload.data.email,
password: payload.data.password,
},
});
toast.add({ title: "Success", description: "Login successful" });
await refreshSession();
await navigateTo("/admin/dashboard");
} catch (error) {
const err = error as NuxtError;
toast.add({
title: "Error",
description: err.statusMessage || "Login failed 🚩",
color: "error",
});
} finally {
loading.value = false;
}
}
</script>
<template>
<div class="flex flex-col items-center justify-center gap-4 p-4">
<pre>
user: {{ user }}
</pre>
<pre>
loggedIn {{ loggedIn }}
</pre>
<UPageCard class="w-full max-w-md">
<UAuthForm
:schema="loginSchema"
:fields="fields"
:providers="providers"
title="Login to your account"
icon="i-lucide-lock"
@submit="onSubmit"
:loading="loading"
>
<template #description>
Don't have an account?
<ULink
to="/register"
class="text-primary font-medium"
>Sign up</ULink
>.
</template>
<template #password-hint>
<ULink
to="/auth/forgot-password"
class="text-primary font-medium"
tabindex="-1"
>Forgot password?</ULink
>
</template>
<template #validation>
<UAlert
v-if="serverError"
color="error"
icon="i-lucide-info"
:title="serverError"
/>
</template>
<template #footer>
By signing in, you agree to our
<ULink
to="#"
class="text-primary font-medium"
>Terms of Service</ULink
>.
</template>
</UAuthForm>
</UPageCard>
</div>
</template>Ejemplo simple login/logout con server/api y $fetch
server\api\login.post.ts
import { z } from "zod";
const bodySchema = z.object({
email: z.email(),
password: z.string().min(6),
});
export default defineEventHandler(async (event) => {
// const { email, password } = await readBody(event); // SIN VALIDACIÓN
const { email, password } = await readValidatedBody(event, bodySchema.parse);
// Here you would typically validate the email and password,
// check them against a database, and create a session or token.
if (email === "test1@test.com" && password === "123123") {
setCookie(event, "auth_token", "un_valor_de_token_seguro", {
httpOnly: true,
path: "/",
});
return {
message: "Login successful",
};
}
throw createError({
statusCode: 401,
statusMessage: "Invalid email or password",
});
});app\pages\login.vue
<script setup lang="ts">
import type { NuxtError } from "#app";
const email = ref<string>("");
const password = ref<string>("");
const responseMessage = ref<string | undefined>("");
const onSubmit = async () => {
responseMessage.value = "";
try {
const response = await $fetch("/api/login", {
method: "POST",
body: {
email: email.value,
password: password.value,
},
});
responseMessage.value = response.message;
// Usa refreshCookie o fuerza una recarga para asegurar que la cookie esté disponible antes de proteger la ruta.
refreshCookie("auth_token");
setTimeout(async () => {
await navigateTo("/dashboard");
}, 1000);
} catch (error) {
const err = error as NuxtError;
responseMessage.value = err.statusMessage;
} finally {
}
};
</script>
<template>
<div>
<form @submit.prevent="onSubmit">
<input
type="email"
placeholder="email"
name="email"
v-model="email"
/>
<input
type="password"
placeholder="password"
name="password"
v-model="password"
/>
<button type="submit">Login</button>
</form>
<p v-if="responseMessage">{{ responseMessage }}</p>
</div>
</template>app\middleware\auth.ts
export default defineNuxtRouteMiddleware((to) => {
// Solo ejecutar en el servidor
if (import.meta.server && to.path.startsWith("/dashboard")) {
const authToken = useCookie("auth_token");
if (!authToken.value) {
console.log("entró");
return navigateTo("/login");
}
}
});server\api\logout.post.ts
// server/api/logout.post.ts
export default defineEventHandler(async (event) => {
// Si usas nuxt-auth-utils:
// await clearUserSession(event)
// Si manejas la cookie manualmente:
setCookie(event, "auth_token", "", { maxAge: 0, path: "/" });
return { success: true };
});app\pages\dashboard\index.vue
<script setup lang="ts">
definePageMeta({
middleware: "auth",
});
async function logout() {
await $fetch("/api/logout", { method: "POST" });
await navigateTo("/login");
}
</script>
<template>
<div>
<h1>Dashboard</h1>
<p>Ruta protegida</p>
<button @click="logout">Logout</button>
</div>
</template>