Módulo 01: Entorno y create-next-app
Objetivos del módulo
- Preparar el equipo: versión de Node.js, gestor de paquetes, editor y navegador
- Crear un proyecto con
create-next-app, entender cada pregunta y cada opción, y saber qué cambia entre Next.js 16 y 15 - Recorrer la estructura generada fichero a fichero y saber cuáles se editan, cuáles se generan y cuáles van a Git
- Leer y modificar
package.jsonynext.config.ts - Usar la CLI de
next:dev,build,start,typegen,info,analyze,upgradeytelemetry - Entender qué es Turbopack, qué guarda en disco y por qué
next devescribe en.next/dev - Trabajar con el servidor de desarrollo: Fast Refresh, overlay de errores, indicador y logs del navegador en el terminal
- Configurar TypeScript con los tipos que genera Next:
next-env.d.ts,PageProps,LayoutPropsy rutas tipadas - Usar ESLint 9 con flat config ahora que
next lintya no existe - Conocer
AGENTS.md, la documentación que viaja dentro denode_modulesy el servidor MCP del modo desarrollo - Montar el esqueleto de la aplicación EMPRESA que crecerá durante todo el curso
💡 Consejo: Este curso usa Next.js 16.4. Instala siempre el último parche de la serie 16.4.x: los parches corrigen errores y fallos de seguridad sin cambiar las APIs, y todo lo que ves aquí funciona igual en cualquiera de ellos. Las salidas de terminal que aparecen en el módulo son reales y muestran el número de parche con el que se obtuvieron. Si tu proyecto sigue en Next.js 15, busca las cajas 🆕 y 🔄: te dicen qué no tendrás y qué hacer en su lugar.
1. Lo que necesitas antes de empezar
Next.js es un framework de React que se ejecuta sobre Node.js. Node se encarga de tres trabajos: ejecutar las herramientas de desarrollo (el compilador, el servidor de desarrollo, ESLint, TypeScript), ejecutar tu código de servidor (Server Components, Server Actions, Route Handlers) y, en producción, servir la aplicación con next start. Por eso, a diferencia de una SPA hecha con Vite, el Node de tu equipo no es solo una herramienta de compilación: es el motor sobre el que corre la mitad de tu aplicación.
1.1 Node.js 🔄 16.0
🔄 Cambia en Next.js 16.0. En Next.js 15 la versión mínima de Node.js es 18.18, y en Next.js 16 es la 20.9. Si usas Next.js 15: puedes seguir con Node 18.18 o posterior, aunque Node 18 ya no recibe parches de seguridad; usa igualmente Node 20.9 o superior y no tendrás que cambiar nada al actualizar.
La versión mínima aparece en el campo engines del package.json del paquete next. Así está en los dos proyectos generados para este módulo:
mi-app-16/node_modules/next/package.json: "node": ">=20.9.0"
mi-app-15/node_modules/next/package.json: "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
Comprueba tus versiones antes de nada:
node -v
npm -v
En el equipo donde se ha preparado este curso la respuesta es:
v24.19.0
11.6.2
Cualquier versión LTS de Node igual o superior a la 20.9 vale. Si estás empezando, instala la LTS más reciente desde nodejs.org. Si trabajas en varios proyectos que piden versiones distintas, usa un gestor de versiones como nvm (Linux y macOS), nvm-windows, fnm o Volta: te permiten tener varias versiones instaladas y cambiar de una a otra por proyecto.
📘 Concepto: El campo
engineses una declaración, no una barrera.npm installsolo avisa si tu Node no cumple el requisito (salvo que activesengine-strict). El error de verdad llega después, al arrancar: Next comprueba la versión de Node y se niega a ejecutarse si es demasiado antigua. Por eso conviene mirarnode -vantes de culpar al código.
1.2 Gestor de paquetes
create-next-app funciona con los cuatro gestores habituales y detecta cuál has usado para lanzarlo:
| Gestor | Crear proyecto | Arrancar en desarrollo |
|---|---|---|
| npm | npx create-next-app@latest | npm run dev |
| pnpm | pnpm create next-app | pnpm dev |
| Yarn | yarn create next-app | yarn dev |
| Bun | bun create next-app | bun dev |
En este curso usaremos npm, que viene con Node y no exige instalar nada más. Si prefieres otro, los comandos son equivalentes. Lo importante es no mezclar gestores en un mismo proyecto: cada uno escribe su propio fichero de bloqueo (package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock) y tener dos produce instalaciones distintas según quién ejecute el comando. Además, como verás en el apartado 5.4, Next mira los ficheros de bloqueo para decidir cuál es la raíz del proyecto.
📘 Concepto:
npxdescarga un paquete a una caché temporal y ejecuta su comando sin instalarlo en el proyecto. Por esonpx create-next-app@latestsiempre usa la última versión publicada del generador, aunque no la tengas instalada. El sufijo@latestes la etiqueta de npm que apunta a la versión estable más reciente; puedes poner una versión concreta (@16.4) o una etiqueta distinta (@canary,@backport).
1.3 Editor
Cualquier editor sirve, pero Visual Studio Code (o Cursor, que se basa en él) tiene dos ventajas concretas con Next.js:
-
El plugin de TypeScript de Next. El
tsconfig.jsonque generacreate-next-appdeclara un plugin llamadonext. Ese plugin avisa en el editor de errores que TypeScript solo no detecta: valores no válidos en la configuración de un segmento de ruta, hooks comouseStateusados en un Server Component, o la directiva'use client'mal colocada. Para activarlo tienes que decirle a VS Code que use la versión de TypeScript del proyecto: abre la paleta de comandos (Ctrl+Shift+P), busca TypeScript: Select TypeScript Version y elige Use Workspace Version. -
Etiquetas de pestaña personalizadas. En el App Router muchos ficheros se llaman igual (
page.tsx,layout.tsx,route.ts), y el editor se llena de pestañas idénticas. La documentación de Next recomienda esta configuración en.vscode/settings.json, que muestra las dos carpetas que contienen cada fichero:
{
"workbench.editor.customLabels.patterns": {
"**/app/**/page.tsx": "${dirname(1)}/${dirname} - page.tsx",
"**/app/**/layout.tsx": "${dirname(1)}/${dirname} - layout.tsx",
"**/app/**/loading.tsx": "${dirname(1)}/${dirname} - loading.tsx",
"**/app/**/error.tsx": "${dirname(1)}/${dirname} - error.tsx",
"**/app/**/not-found.tsx": "${dirname(1)}/${dirname} - not-found.tsx",
"**/app/**/template.tsx": "${dirname(1)}/${dirname} - template.tsx",
"**/app/**/default.tsx": "${dirname(1)}/${dirname} - default.tsx",
"**/app/**/route.ts": "${dirname(1)}/${dirname} - route.ts"
}
}
Con ella, app/empleados/[id]/page.tsx aparece como empleados/[id] - page.tsx en lugar de un simple page.tsx. Los IDE de JetBrains (WebStorm, IntelliJ) ya muestran la carpeta de los ficheros con nombre repetido y no necesitan nada.
Extensiones útiles: ESLint (muestra los avisos del linter mientras escribes) y Tailwind CSS IntelliSense (autocompleta las clases de utilidad).
1.4 Navegadores soportados 🔄 16.0
🔄 Cambia en Next.js 16.0. Next.js 16 sube el mínimo a Chrome 111, Edge 111, Firefox 111 y Safari 16.4, y genera código para esos navegadores. Si usas Next.js 15: el mínimo es más bajo (Next 15 todavía admitía navegadores algo más antiguos); si necesitas dar soporte a navegadores anteriores a los de la lista, es un motivo real para quedarte en 15 o para añadir polyfills.
Para desarrollar, usa un navegador moderno con las herramientas de desarrollo abiertas. Instala también React Developer Tools: el propio Next te lo recuerda en la consola del navegador cada vez que carga una página en desarrollo, como verás en el apartado 6.
1.5 La API EMPRESA y el puerto 3000
Durante los primeros módulos los datos vendrán de la API EMPRESA, un servidor Node independiente que se distribuye con el curso en recursos/empresa-api.zip. Se descomprime y se arranca así:
cd empresa-api
node server.mjs
API EMPRESA en http://localhost:3000/api
Usuarios: ana.garcia@empresa.es (ADMIN) y luis.martinez@empresa.es (USER), contraseña "secreto"
Aquí aparece el primer choque: Next.js también usa el puerto 3000 por defecto. Si arrancas los dos sin pensar, uno de ellos no podrá escuchar. A lo largo del curso la API se queda en el 3000 y Next se arranca en el 3001. En el apartado 4.3 verás las tres formas de cambiar el puerto, y en el caso práctico lo dejarás fijado en el package.json.
⚠️ Importante: En Windows, Turbopack escribe en
.nextficheros cuyo nombre incluye partes de la ruta de tu proyecto. Si el proyecto está muy anidado (por ejemplo, dentro de varias carpetas de OneDrive con nombres largos), puedes superar el límite de longitud de ruta de Windows y ver un error comopath length ... exceeds max length of filesystem. Crea tus proyectos en rutas cortas, comoC:\dev\empresa.
2. Crear el proyecto con create-next-app
2.1 El primer proyecto
create-next-app es el generador oficial. Crea la carpeta, escribe los ficheros de la plantilla, instala las dependencias, genera los tipos de las rutas e inicializa un repositorio Git. La forma más rápida de usarlo es aceptar los valores recomendados con --yes:
npx create-next-app@latest empresa --yes
cd empresa
npm run dev -- -p 3001
Esta es la salida real de create-next-app 16.4.3 (se han quitado los códigos de color):
Creating a new Next.js app in C:\proyectos\mi-app-16.
Using npm.
Initializing project with template: app-tw
Installing dependencies:
- next
- react
- react-dom
Installing devDependencies:
- @tailwindcss/turbopack
- @types/node
- @types/react
- @types/react-dom
- eslint
- eslint-config-next
- tailwindcss
- typescript
npm warn deprecated eslint@9.39.5: This version is no longer supported. Please see https://eslint.org/version-support for other options.
added 359 packages, and audited 360 packages in 28m
145 packages are looking for funding
run `npm fund` for details
16 moderate severity vulnerabilities
...
Generating route types...
✓ Types generated successfully
Initialized a git repository.
Success! Created mi-app-16 at C:\proyectos\mi-app-16
Fíjate en varias cosas:
template: app-tw: la plantilla es la del App Router (app) con Tailwind (tw). Hay otras:app-api,app-empty,default(Pages Router)…- 359 paquetes. Casi todos los trae ESLint con sus plugins. El proyecto equivalente de Next.js 15, que no instala ESLint, se queda en 47 (lo verás en el apartado 2.5).
- El aviso de ESLint 9. La plantilla pide
eslint@^9, y en el momento de escribir esto ESLint 10 ya ha salido, así que npm marca la 9 como sin soporte. No es un error:eslint-config-next16.4 admite ESLint 9 y 10. Lo verás en detalle en el apartado 9. - Las vulnerabilidades de
npm audit. En un proyecto recién creado suelen estar en dependencias de las herramientas (ESLint y sus plugins), no en el código que llega al navegador. Revisa el informe connpm audit, pero no ejecutesnpm audit fix --forcea ciegas: puede cambiar versiones mayores y romper el proyecto. Generating route types...: al terminar, el generador ejecutanext typegenpara que el editor tenga desde el primer momento los tipos de las rutas. Lo explica el apartado 8.- 28 minutos es lo que tardó en una carpeta temporal con un antivirus muy activo. En un equipo normal tarda de uno a tres minutos. Si se te hace eterno, excluye la carpeta de desarrollo del análisis en tiempo real del antivirus.
⚠️ Importante: Hay un detalle del comando
npm run dev -- -p 3001. Los dos guiones--le dicen a npm que lo que sigue no es para él, sino para el script. Sin ellos, npm se quedaría con-py Next arrancaría en el 3000. Con pnpm, Yarn o Bun no hacen falta.
Abre http://localhost:3001 y verás la página de bienvenida de la plantilla:

Figura 1. La plantilla de Next.js 16.4 en modo desarrollo. El círculo con la «N» abajo a la izquierda es el indicador de desarrollo.
2.2 Las preguntas del modo interactivo
Si no pasas --yes, el generador te pregunta. La primera pregunta, después del nombre, es si quieres los valores recomendados:
What is your project named? my-app
Would you like to use the recommended Next.js defaults?
Yes, use recommended defaults - TypeScript, ESLint, No React Compiler, Tailwind CSS, No src/ directory, App Router, Cache Components, AGENTS.md, Agent feedback
No, reuse previous settings
No, customize settings - Choose your own preferences
Si eliges customize settings, llegan las preguntas una a una:
Would you like to use TypeScript? No / Yes
Which linter would you like to use? ESLint / Biome / None
Would you like to use React Compiler? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like your code inside a `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to use Cache Components? No / Yes
Would you like to customize the import alias (`@/*` by default)? No / Yes
What import alias would you like configured? @/*
Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code? No / Yes
Would you like to help improve Next.js by letting agents prepare anonymized feedback for your review as you code? (Disable anytime with `experimental.agentFeedback: false`.) No / Yes
Qué significa cada una y qué te conviene en este curso:
| Pregunta | Recomendado | Qué hace |
|---|---|---|
| TypeScript | Sí | Genera .tsx, tsconfig.json y los tipos de React y Node. Todo el curso usa TypeScript |
| Linter | ESLint | Añade eslint.config.mjs y el script lint. Biome es la alternativa rápida con menos reglas |
| React Compiler | No, de momento | Activa la memorización automática de componentes. Se verá en el módulo de rendimiento |
| Tailwind CSS | Sí | Añade Tailwind 4 y @import "tailwindcss" en globals.css |
Carpeta src/ | No | Si dices que sí, app/ va dentro de src/ y el alias @/* apunta a ./src/* |
| App Router | Sí | Si dices que no, genera el Pages Router (pages/), el modelo anterior a Next 13 |
| Cache Components | Sí | Escribe cacheComponents: true y partialPrefetching: true en next.config.ts |
| Alias de importación | @/* | Te permite escribir import { x } from '@/lib/x' en lugar de rutas relativas largas |
AGENTS.md | Sí | Crea las instrucciones para asistentes de IA (apartado 10) |
| Agent feedback | A tu elección | Permite que un agente de IA prepare informes anónimos de fallos de Next para que tú los revises |
📘 Concepto:
--yesno significa siempre «los valores por defecto». Significa «usa las preferencias guardadas o, si no hay, los valores por defecto».create-next-apprecuerda lo que contestaste la última vez que personalizaste un proyecto. Si un día eliges Biome y sinsrc/, el siguiente--yesrepetirá esa elección. Para volver a empezar, añade--reset-preferences(o--reset).
Hay una diferencia sutil entre --yes y elegir Yes, use recommended defaults en el menú. Revisando el código de create-next-app 16.4.3 se ve que tiene dos juegos de valores: el que usa --yes no incluye agent feedback y el del menú recomendado sí ({...valoresPorDefecto, agentFeedback: true}). Por eso el proyecto creado con --yes en este módulo no tiene experimental.agentFeedback en next.config.ts, aunque la documentación diga que la configuración recomendada lo activa.
2.3 Todas las opciones de la línea de órdenes
Cada pregunta tiene su opción para usarla sin interacción. Además, cualquier opción booleana se niega con el prefijo --no- (--no-tailwind, --no-src-dir, --no-agents-md). Esta es la ayuda real de create-next-app 16.4.3 (npx create-next-app@16.4.3 --help), resumida:
| Opción | Efecto |
|---|---|
--ts, --typescript / --js, --javascript | TypeScript (por defecto) o JavaScript |
--tailwind | Tailwind CSS (por defecto) |
--react-compiler | Activa React Compiler |
--cache-components | Activa Cache Components (por defecto con el App Router) |
--eslint / --biome / --no-linter | Elige el linter |
--app | App Router |
--src-dir | Código dentro de src/ |
--rspack | Usa Rspack como bundler en lugar de Turbopack |
--import-alias <prefijo/*> | Alias de importación (por defecto @/*) |
--api | Proyecto solo con Route Handlers, sin páginas |
--empty | Proyecto mínimo, sin la página de bienvenida |
--use-npm, --use-pnpm, --use-yarn, --use-bun | Fuerza un gestor de paquetes |
--skip-install | No instala dependencias |
--disable-git | No inicializa un repositorio Git |
--agents-md | Crea AGENTS.md (por defecto) |
--agent-feedback / --no-agent-feedback | Activa o no el feedback de agentes |
--reset, --reset-preferences | Olvida las preferencias guardadas |
--yes | Usa preferencias guardadas o valores por defecto en todo lo que no indiques |
-e, --example <nombre\|url> | Parte de un ejemplo oficial o de un repositorio de GitHub |
Con --example puedes partir de cualquiera de los ejemplos del repositorio de Next (npx create-next-app@latest --example with-docker mi-app) o de una URL de GitHub. Los ejemplos no siempre están al día con la última versión: revisa su package.json antes de usarlos como base.
2.4 Variantes probadas
Para ver qué cambia con cada opción, se han generado tres proyectos más con --skip-install --disable-git (así no se instala nada y solo quedan los ficheros):
npx create-next-app@16.4.3 solo-api --api --yes --skip-install --disable-git
npx create-next-app@16.4.3 vacia --empty --yes --skip-install --disable-git
npx create-next-app@16.4.3 con-src --src-dir --no-tailwind --no-agents-md --yes --skip-install --disable-git
Los ficheros que genera cada uno:
=== solo-api
solo-api/.gitignore
solo-api/AGENTS.md
solo-api/app/[slug]/route.ts
solo-api/app/route.ts
solo-api/next.config.ts
solo-api/next-env.d.ts
solo-api/package.json
solo-api/README.md
solo-api/tsconfig.json
=== vacia
vacia/.gitignore
vacia/AGENTS.md
vacia/app/globals.css
vacia/app/layout.tsx
vacia/app/page.tsx
vacia/eslint.config.mjs
vacia/next.config.ts
vacia/next-env.d.ts
vacia/package.json
vacia/README.md
vacia/tsconfig.json
=== con-src
con-src/.gitignore
con-src/eslint.config.mjs
con-src/next.config.ts
con-src/next-env.d.ts
con-src/package.json
con-src/public/file.svg
con-src/public/globe.svg
con-src/public/next.svg
con-src/public/vercel.svg
con-src/public/window.svg
con-src/README.md
con-src/src/app/favicon.ico
con-src/src/app/globals.css
con-src/src/app/layout.tsx
con-src/src/app/page.module.css
con-src/src/app/page.tsx
con-src/tsconfig.json
Lo que se aprende de ellos:
--apino instala React. Sus dependencias son solonext, y en desarrollotypescript,@types/nodey@types/react. Tampoco trae ESLint. Genera dos Route Handlers de ejemplo, uno de ellos con un segmento dinámico:
// solo-api/app/[slug]/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> },
) {
const { slug } = await params;
return NextResponse.json({ message: `Hello ${slug}!` });
}
Fíjate en que params es una Promise: en Next.js 16 los parámetros de ruta son siempre asíncronos. Lo verás en el módulo 04 y los Route Handlers en el módulo 12.
--emptymantiene Tailwind y ESLint, pero deja una página mínima sinpublic/:
export default function Home() {
return (
<main>
<div>Hello world!</div>
</main>
);
}
--src-dirmueveapp/asrc/app/y cambia el alias: entsconfig.jsonaparece"@/*": ["./src/*"].public/y los ficheros de configuración siguen en la raíz. Sin Tailwind, la página usa un CSS Module (page.module.css) ynext.config.tsya no tiene la regla de Turbopack para Tailwind. Con--no-agents-mdno hayAGENTS.md.
💡 Consejo: ¿
src/sí o no? Es cuestión de gusto. Sinsrc/, la raíz mezclaapp/con los ficheros de configuración. Consrc/, todo tu código queda en una carpeta y la raíz solo tiene configuración. En este curso se usa la opción por defecto, sinsrc/, para que las rutas de los ejemplos sean más cortas. Si eligessrc/, recuerda quepublic/,next.config.tsy los.envno se mueven.
2.5 Qué genera Next.js 16 frente a Next.js 15 🔄 16.0
🔄 Cambia en Next.js 16.0. La plantilla de
create-next-app15.5 con--yesno instala ESLint, usa Tailwind a través de PostCSS, dejanext.config.tsvacío y añade--turbopacka los scripts. Si usas Next.js 15: crea el proyecto connpx create-next-app@15 mi-app --yes --eslintsi quieres ESLint, y ten en cuenta que no tendrásAGENTS.md, Cache Components ni los tipos denext typegenentsconfig.jsonhasta que actualices.
Para comparar con datos reales se han generado los dos proyectos en la misma máquina y el mismo día:
npx create-next-app@16.4.3 mi-app-16 --yes
npx create-next-app@15.5.28 mi-app-15 --yes
El package.json de cada uno:
{
"name": "mi-app-16",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"next": "16.4.3",
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"@tailwindcss/turbopack": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.4.3",
"tailwindcss": "^4",
"typescript": "^5"
}
}
{
"name": "mi-app-15",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"start": "next start"
},
"dependencies": {
"react": "19.1.0",
"react-dom": "19.1.0",
"next": "15.5.28"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4"
}
}
Resumen de las diferencias comprobadas:
| Aspecto | Next.js 16.4.3 | Next.js 15.5.28 |
|---|---|---|
React en package.json | 19.3.0 | 19.1.0 |
| React que usa el App Router | 19.3.0-canary-278794d7-20261002 | 19.2.0-canary-0bdb9206-20250818 |
| Scripts | next dev, next build, lint: eslint | next dev --turbopack, next build --turbopack, sin lint |
ESLint con --yes | Sí, eslint.config.mjs | No |
| Tailwind | Cargador @tailwindcss/turbopack en next.config.ts | @tailwindcss/postcss en postcss.config.mjs |
next.config.ts | cacheComponents, partialPrefetching, regla de Turbopack | Vacío |
AGENTS.md | Sí | No |
tsconfig.json jsx | react-jsx | preserve |
tsconfig.json include | Añade .next/dev/types/**/*.ts y **/*.mts | Solo .next/types/**/*.ts |
Tipo de children en el layout | LayoutProps<"/"> | Readonly<{ children: React.ReactNode }> |
| Paquetes instalados | 359 | 47 |
Documentación en node_modules/next/dist/docs | Sí | No |
Las dos plantillas tampoco se parecen visualmente. Compara la figura 1 con la de Next.js 15:

Figura 2. La plantilla de Next.js 15.5, para comparar con la figura 1. El indicador de desarrollo ya existía, con otro diseño.
📘 Concepto: ¿Por qué el App Router usa un React distinto del que pone tu
package.json? Next.js incluye dentro denext/dist/compiled/su propia copia de React, una versión canary que contiene todo lo estable de React 19 más funciones que los frameworks están validando. El App Router usa siempre esa copia. Elreactde tupackage.jsonlo usan el Pages Router, las herramientas (ESLint, TypeScript, las pruebas) y las bibliotecas de terceros que lo importen. Por eso debes declararlo igualmente, aunque el App Router no lo use. Para saber qué React ejecuta tu aplicación de verdad, miranode_modules/next/dist/compiled/react/, no tupackage.json.
2.6 Elegir la versión al crear el proyecto
@latest crea siempre un proyecto con la versión estable más reciente. Si necesitas otra, fija la versión del generador: cada versión de create-next-app instala la versión de next correspondiente.
npx create-next-app@16.4 empresa --yes # último parche de la serie 16.4
npx create-next-app@15 empresa15 --yes # último Next.js 15 (15.5.x)
npx create-next-app@canary prueba --yes # versión de pruebas: nunca en producción
También puedes montar un proyecto a mano, sin generador. La documentación oficial lo explica en cuatro pasos: instalar paquetes, añadir los scripts, crear app/layout.tsx y crear app/page.tsx.
mkdir empresa-manual && cd empresa-manual
npm init -y
npm i next@latest react@latest react-dom@latest
npm i -D typescript @types/react @types/node
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es">
<body>{children}</body>
</html>
);
}
// app/page.tsx
export default function Page() {
return <h1>Hola, Next.js</h1>;
}
Al ejecutar npm run dev por primera vez, Next detecta que usas TypeScript y crea él mismo tsconfig.json y next-env.d.ts. Si olvidas el layout raíz, next dev lo crea también. El montaje manual es útil para entender las piezas, pero en la práctica create-next-app te ahorra errores de configuración.
3. Recorrido por el proyecto generado
Figura 3. Lo que hay en un proyecto recién creado. Los dos bloques de arriba los editas tú; el de abajo lo generan las herramientas.
3.1 La carpeta app/
app/ es el App Router: cada carpeta dentro de app es un segmento de la URL, y una carpeta se convierte en ruta pública cuando contiene un fichero page.tsx (o un route.ts, para una API). Lo verás a fondo en el módulo 03. De momento basta con los cuatro ficheros que trae la plantilla.
app/layout.tsx es el layout raíz. Envuelve todas las páginas de la aplicación y es el único sitio donde se escriben <html> y <body>. Es obligatorio.
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
}
Línea a línea:
import type { Metadata } from "next": el tipo del objeto de metadatos.import typeindica que solo se importa un tipo, que desaparece al compilar.next/font/google: descarga las fuentes Geist al compilar y las sirve desde tu propio dominio. El navegador no hace ninguna petición a Google. Cada llamada (Geist({...})) devuelve un objeto con una propiedadvariable, que es el nombre de una clase CSS que define la variable--font-geist-sans. Las fuentes se verán en el módulo 06.import "./globals.css": los estilos globales se importan como un módulo más. Solo se pueden importar CSS globales desde un layout o una página.export const metadata: Next convierte este objeto en las etiquetas<title>y<meta name="description">del<head>. Por eso no escribes<head>a mano. Lo verás en el módulo 07.LayoutProps<"/">: un tipo global (no se importa) que Next genera a partir de tus rutas. Dice que este layout, el de la ruta/, recibechildren. Lo explica el apartado 8.lang="en": cámbialo alang="es"en tus proyectos. Lo usan los lectores de pantalla y los traductores automáticos.
💡 Consejo: La plantilla tiene una pequeña trampa:
globals.cssdeclarabody { font-family: Arial, Helvetica, sans-serif; }, que tiene más prioridad que la utilidad de Tailwindfont-sansasociada a Geist. El texto normal de la página sale en Arial aunque la fuente Geist se descargue. Si quieres Geist en todo el documento, quita esa línea deglobals.csso añadefont-sansal<body>.
app/page.tsx es la página de la ruta /. Es un componente de React exportado por defecto. En la plantilla usa next/image para el logotipo y un montón de clases de Tailwind; lo sustituirás enseguida.
app/globals.css carga Tailwind y define unas variables de color con modo oscuro automático:
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
@import "tailwindcss" es la forma de cargar Tailwind 4: ya no hay tailwind.config.js, la configuración se hace en CSS con @theme. Si necesitas repasar Tailwind 4, tienes el Curso de Tailwind CSS. Este fichero es idéntico en las plantillas de 15 y 16.
app/favicon.ico es un ejemplo de convención de fichero: un favicon.ico en app/ se convierte automáticamente en el icono de la web, sin que escribas ninguna etiqueta <link>.
3.2 La carpeta public/
Lo que pongas en public/ se sirve tal cual desde la raíz del sitio: public/next.svg está en /next.svg. Úsala para imágenes, documentos descargables, robots.txt estáticos o la carpeta de imágenes de los productos de EMPRESA. No pongas ahí nada privado: todo lo que hay en public/ lo puede descargar cualquiera.
⚠️ Importante: No crees en
public/un fichero con la misma ruta que una página (por ejemplo,public/acercayapp/acerca/page.tsx). La documentación lo marca como error en el Pages Router, y en el App Router solo consigues confusión sobre qué se sirve. Mantén los estáticos en subcarpetas propias, comopublic/img/.
3.3 Ficheros de configuración de la raíz
| Fichero | Para qué sirve | ¿A Git? |
|---|---|---|
package.json | Dependencias y scripts. Apartado 4 | Sí |
package-lock.json | Versiones exactas instaladas, para que todos tengan lo mismo | Sí |
next.config.ts | Configuración de Next. Apartado 3.4 | Sí |
tsconfig.json | Configuración de TypeScript. Apartado 8 | Sí |
eslint.config.mjs | Configuración de ESLint. Apartado 9 | Sí |
AGENTS.md | Instrucciones para asistentes de IA. Apartado 10 | Sí |
README.md | Instrucciones para personas | Sí |
.gitignore | Lo que Git debe ignorar | Sí |
next-env.d.ts | Referencias a los tipos de Next. Lo regeneran las herramientas | No |
.next/ | Salida de compilación y caché | No |
node_modules/ | Paquetes instalados | No |
.env* | Variables de entorno, a menudo con secretos | No (salvo que lo decidas) |
El .gitignore de la plantilla ya refleja la tabla. Estas son sus líneas más importantes:
# dependencies
/node_modules
# next.js
/.next/
/out/
# env files (can opt-in for committing if needed)
.env*
# typescript
*.tsbuildinfo
next-env.d.ts
Ignora todos los .env*. Es la opción segura: si algún día quieres subir un .env sin secretos (por ejemplo, con valores por defecto), tendrás que hacerlo explícito con una línea !.env.example. Las variables de entorno se ven en el módulo 13.
⚠️ Importante:
next-env.d.tsse ignora desde Next.js 16. En proyectos antiguos puede estar en el repositorio: si es así, bórralo de Git congit rm --cached next-env.d.tsy añádelo al.gitignore. Next lo regenera en cadadev,buildotypegen, y su contenido cambia entre versiones y entre desarrollo y producción, así que versionarlo solo produce cambios espurios.
3.4 next.config.ts
Es el fichero de configuración de Next. Al ser TypeScript, el tipo NextConfig te da autocompletado y te avisa si escribes una opción que no existe. El que genera la plantilla 16.4:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
cacheComponents: true,
partialPrefetching: true,
turbopack: {
rules: {
"*.css": {
loaders: ["@tailwindcss/turbopack"],
as: "*.css",
},
},
},
};
export default nextConfig;
Al arrancar, Next te recuerda qué opciones importantes están activas:
✓ Running next.config.ts took 1471ms
- Cache Components enabled
- Partial Prefetching enabled
Las tres opciones de la plantilla:
cacheComponents: true activa Cache Components, el modelo de caché y renderizado que Next recomienda desde la 16.4 y que será el predeterminado en Next 17. Con él, Next prerenderiza al compilar todo lo que puede de cada página, y te obliga a decidir qué hacer con los datos que no son estáticos: envolverlos en <Suspense> para que lleguen en streaming o guardarlos en caché con 'use cache'. Tiene una consecuencia que notarás pronto: next build falla si una página lee datos dinámicos fuera de <Suspense>. Lo verás en el apartado 7.3, y la caché a fondo en el módulo 09.
partialPrefetching: true hace que, al precargar los enlaces, Next pida solo la parte estática de cada ruta. Necesita cacheComponents; sin él, next dev y next build fallan al validar la configuración. Se ve en el módulo 15.
turbopack.rules registra un loader para los ficheros CSS: cada .css pasa por @tailwindcss/turbopack, que genera las utilidades de Tailwind. Es la alternativa de la plantilla 16 al postcss.config.mjs de la 15.
⚠️ Importante: La regla
"*.css"de la plantilla 16.4 rompe los CSS Modules: un ficheroX.module.csspasa a tratarse como CSS global, elimport styles from './X.module.css'devuelve un objeto vacío y los elementos se quedan sin clase, sin ningún error en la compilación. Si vas a usar CSS Modules, aplica uno de los arreglos del módulo 06 (una regla conconditionpara los.module.css, o volver a@tailwindcss/postcss).
Cache Components y Partial Prefetching en Next.js 15 🆕 16.0
🆕 Desde Next.js 16.0.
cacheComponentses una opción estable de primer nivel desde Next.js 16.0, ypartialPrefetchingllega en la 16.3. En Next.js 15 no existen como opciones estables (soloexperimental.cacheComponents,experimental.ppryexperimental.dynamicIO, pensadas para las versiones canary). Si usas Next.js 15: dejanext.config.tssin ellas y usa el modelo de caché de Next 15 (fetchconcacheynext.revalidate,export const revalidate,unstable_cache), que se explica en el módulo 09.
Opciones que usarás a menudo
next.config.ts admite muchas opciones; la documentación local tiene una página para cada una en node_modules/next/dist/docs/01-app/03-api-reference/05-config/01-next-config-js/. Estas son las que aparecerán en el curso:
| Opción | Para qué | Módulo |
|---|---|---|
typedRoutes | Comprueba en tiempo de compilación que los href de <Link> existen | 01 (apartado 8.4) |
logging | Qué se registra en el terminal: peticiones, fetch, logs del navegador | 01 (apartado 6.4) |
images | Dominios y tamaños permitidos para next/image | 07 |
redirects, rewrites, headers | Redirecciones, reescrituras y cabeceras HTTP | 13 |
reactCompiler | Activa React Compiler | 20 |
output | 'standalone' para Docker, 'export' para sitio estático | 23 |
turbopack | Reglas de loaders, alias y raíz del proyecto | 01 (apartado 5) |
Si la configuración es incorrecta, Next lo dice al arrancar y no sigue. Y si cambias next.config.ts con next dev en marcha, Next reinicia el servidor solo.
📘 Concepto:
next.config.tsse ejecuta en Node, no en el navegador ni dentro de tu aplicación. Puedes leer variables de entorno, importar otros ficheros o calcular valores. Como se carga en CommonJS, elawaitde nivel superior no funciona ennext.config.ts; si lo necesitas, renómbralo anext.config.mts(con Node 22.18 o posterior, que entiende TypeScript de forma nativa).
4. Scripts y la CLI de next
4.1 Los scripts del package.json
| Script | Comando | Cuándo |
|---|---|---|
npm run dev | next dev | Mientras programas: compila bajo demanda y recarga al guardar |
npm run build | next build | Antes de desplegar: compila todo para producción |
npm start | next start | Sirve lo compilado por build |
npm run lint | eslint | Revisa el código con ESLint |
Los scripts del package.json son atajos: npm run dev ejecuta el binario next instalado en node_modules/.bin. Podrías escribir npx next dev y sería lo mismo.
4.2 Todos los comandos de next
npx next --help en Next.js 16.4.3 lista estos comandos:
build [directory] [options] Creates an optimized production build of your application. The output displays information about each route.
analyze [options] [command] [directory] Analyze production bundle output with an interactive web ui. Does not produce an application build. Only compatible with Turbopack.
dev [directory] [options] Starts Next.js in development mode with hot-code reloading, error reporting, and more.
info [options] Prints relevant details about the current system which can be used to report Next.js bugs.
start [directory] [options] Starts Next.js in production mode. The application should be compiled with `next build` first.
telemetry [options] [arg] Allows you to enable or disable Next.js' completely anonymous telemetry collection.
typegen [directory] [options] Generate TypeScript definitions for routes, pages, and layouts without running a full build.
upgrade [directory] [options] Upgrade Next.js apps to desired versions with a single command.
internal [options] [command] Internal debugging commands. Use with caution. Not covered by semver.
Y estos son los de Next.js 15.5.28:
build [directory] [options] Creates an optimized production build of your application. ...
dev [directory] [options] Starts Next.js in development mode with hot-code reloading, error reporting, and more.
info [options] Prints relevant details about the current system which can be used to report Next.js bugs.
lint [directory] [options] Runs ESLint for all files in the `/src`, `/app`, `/pages`, `/components`, and `/lib` directories. ...
start [directory] [options] Starts Next.js in production mode. The application should be compiled with `next build` first.
telemetry [options] [arg] Allows you to enable or disable Next.js' completely anonymous telemetry collection.
typegen [directory] [options] Generate TypeScript definitions for routes, pages, and layouts without running a full build.
internal [options] [command] Internal debugging commands. Use with caution. Not covered by semver.
En 16 desaparece lint y aparecen analyze y upgrade en la ayuda. Ejecutar next sin comando equivale a next dev.
| Comando | Qué hace | Opciones más útiles |
|---|---|---|
next dev | Servidor de desarrollo | -p puerto, -H host, --webpack, --inspect, --experimental-https |
next build | Compilación de producción | --webpack, --debug-prerender, --analyze, -d (más detalle) |
next start | Servidor de producción | -p, -H, --keepAliveTimeout |
next typegen | Genera los tipos de las rutas sin compilar | [directorio] |
next info | Datos del sistema para informar de un fallo | --verbose |
next telemetry | Activa o desactiva la telemetría anónima | --enable, --disable |
next upgrade | Actualiza Next a otra versión | --revision, --agent |
next analyze | Analizador visual de los paquetes de JavaScript | --output, --port |
4.3 Cambiar el puerto
Hay tres formas, de la más puntual a la más permanente:
# 1. Para una ejecución (los dos guiones son para npm)
npm run dev -- -p 3001
# 2. Con la variable de entorno PORT (bash, Git Bash, macOS, Linux)
PORT=3001 npm run dev
# 2b. La misma variable en PowerShell
$env:PORT = "3001"; npm run dev
{
"scripts": {
"dev": "next dev -p 3001",
"start": "next start -p 3001"
}
}
La tercera, en el package.json, es la que usarás en el proyecto EMPRESA: así nadie del equipo choca con la API. La opción -H (hostname) controla en qué interfaz escucha el servidor. Por defecto es 0.0.0.0, es decir, todas: otro dispositivo de tu red (tu móvil, por ejemplo) puede abrir la aplicación con la IP de tu equipo. Si solo quieres acceso local, usa -H 127.0.0.1.
4.4 next info
Cuando algo falla y vas a pedir ayuda (en un foro o en una issue de GitHub), lo primero que te pedirán es la salida de next info. Esta es la real del proyecto de 16.4.3:
Operating System:
Platform: win32
Arch: x64
Version: Windows 11 Education
Available memory (MB): 65380
Available CPU cores: 12
Binaries:
Node: 24.19.0
npm: 11.6.2
Yarn: N/A
pnpm: 10.15.1
Relevant Packages:
next: 16.4.3 // There is a newer version (16.4.4) available, upgrade recommended!
eslint-config-next: N/A
react: 19.3.0
react-dom: 19.3.0
typescript: 5.9.3
Next.js Config:
output: N/A
⚠ There is a newer version (16.4.4) available, upgrade recommended!
Please try the latest canary version (`npm install next@canary`) to confirm the issue still exists before creating a new issue.
Fíjate en que también te avisa de que hay un parche nuevo. Es la forma más rápida de saber si estás al día.
4.5 next telemetry
Next recoge datos de uso anónimos (qué comandos se usan, qué opciones, cuánto tarda la compilación) para decidir en qué mejorar. No recoge tu código ni tus datos. Puedes consultarlo y desactivarlo:
npx next telemetry # muestra el estado
npx next telemetry --disable # lo desactiva para tu usuario
También se desactiva con la variable de entorno NEXT_TELEMETRY_DISABLED=1, útil en servidores de integración continua. Ten en cuenta que el feedback de agentes del apartado 10 necesita la telemetría activada.
4.6 next upgrade
next upgrade actualiza Next y los paquetes relacionados a la versión que indiques (por defecto, la última del canal que tengas instalado). Con --agent, prepara la actualización para que la haga un asistente de IA con las guías de migración de tu versión:
npx next upgrade # a la última del canal que tengas instalado
npx next upgrade --revision 16.4.4 # a una versión concreta
Para aplicar solo el último parche de tu serie también vale npm install next@16.4 eslint-config-next@16.4. La migración completa de 15 a 16, con los codemods ejecutados de verdad, está en el módulo 24.
5. Turbopack
5.1 Qué es un bundler y por qué Next trae el suyo
Un navegador no puede ejecutar directamente tu proyecto: hay TypeScript, JSX, imports de CSS, módulos de node_modules escritos en formatos distintos y cientos de ficheros. Un bundler recorre todos los import desde cada punto de entrada, transforma cada fichero a JavaScript y CSS que el navegador entiende, y los agrupa en unos pocos ficheros optimizados (chunks).
En Next el trabajo es mayor que en una SPA, porque cada ruta se compila tres veces: para el servidor de React Server Components, para el renderizado HTML en el servidor y para el navegador. Además tiene que respetar la frontera entre Server y Client Components. Turbopack es el bundler de Next, escrito en Rust y diseñado para este trabajo: compila solo lo que se pide y guarda en memoria (y en disco) el resultado de cada función, de forma que al cambiar un fichero solo se recalcula lo que depende de él.
5.2 Turbopack por defecto 🔄 16.0
🔄 Cambia en Next.js 16.0. En Next.js 15 el bundler por defecto es webpack, y Turbopack se activa con
--turbopacken los scripts (la plantilla 15.5 ya lo hace endevybuild). En Next.js 16 Turbopack es el predeterminado ennext devynext build, y webpack se pide con--webpack. Si usas Next.js 15: manténnext dev --turbopackynext build --turbopacken los scripts, como hace la plantilla; sin la opción usarás webpack.
Si tu proyecto tiene una configuración webpack personalizada en next.config (por ejemplo, de un plugin antiguo), Next 16 no la aplica con Turbopack. Tienes dos salidas: traducirla a la sección turbopack (reglas de loaders, resolveAlias) o seguir con webpack:
{
"scripts": {
"dev": "next dev --webpack",
"build": "next build --webpack"
}
}
La salida de la consola dice qué bundler está en uso: ▲ Next.js 16.4.3 (Turbopack). Con --webpack aparece (webpack).
5.3 La caché en disco 🆕 16.1
🆕 Desde Next.js 16.1. Turbopack guarda su trabajo en disco entre ejecuciones de
next dev(en.next/dev/cache/turbopack) desde la 16.1, y también entre ejecuciones denext build(en.next/cache/turbopack) desde la 16.3. No existe en Next.js 15. Si usas Next.js 15: cada arranque denext devvuelve a compilar desde cero; la única alternativa es dejar el servidor en marcha el máximo tiempo posible.
La diferencia se nota al reiniciar. En la primera petición a / de un proyecto recién creado, next dev tardó esto:
○ Compiling / ...
GET / 200 in 15.6s (next.js: 8.4s, application-code: 7.2s)
La línea de cada petición separa el tiempo de Next (next.js, compilar y preparar) del de tu código (application-code, ejecutar tus componentes). Con la caché ya llena, reiniciar el servidor y pedir la misma página baja el tiempo de compilación de forma notable, porque Turbopack reutiliza lo que guardó.
Las dos cachés se controlan con dos opciones experimentales, activadas por defecto:
const nextConfig: NextConfig = {
experimental: {
turbopackFileSystemCacheForDev: true,
turbopackFileSystemCacheForBuild: true,
},
};
Si un día ves un comportamiento raro que no se explica por tu código (un cambio que no aparece, un error que ya arreglaste), para el servidor y borra la carpeta .next. Es la versión Next de «apagar y encender».
💡 Consejo: La caché de
buildsolo acelera si la carpeta.next/cachesobrevive entre compilaciones. En un contenedor Docker o en un servidor de integración continua cada compilación empieza limpia, salvo que configures que esa carpeta se guarde. El módulo 23 explica cómo.
5.4 La raíz del proyecto y el aviso de los lockfiles
Turbopack necesita saber cuál es la raíz del proyecto, porque no puede leer ficheros fuera de ella. La deduce buscando ficheros de bloqueo (package-lock.json y equivalentes) hacia arriba. En el equipo de este curso aparecieron dos avisos reales que te puedes encontrar:
⚠ Warning: Next.js ignored package-lock.json in C:\Users\alumno because it would include your home directory (C:\Users\alumno).
To use this directory, set `turbopack.root` in your Next.js config.
⚠ Warning: Next.js ignored package-lock.json in C:\proyectos because it is outside the current Git repository (C:\proyectos\mi-app-16).
To use this directory, set `turbopack.root` in your Next.js config.
El primero aparece porque alguna vez se ejecutó npm install directamente en la carpeta de usuario, que dejó allí un package-lock.json huérfano. El segundo, porque el proyecto está dentro de otra carpeta que también tiene un package-lock.json. En ambos casos Next hace lo correcto (ignora el fichero ajeno) y solo avisa. La solución limpia es borrar el package-lock.json que sobra si no pertenece a ningún proyecto. Si la estructura es intencionada (un monorepo con varios paquetes), indica la raíz:
import path from "node:path";
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
turbopack: {
root: path.join(__dirname, ".."),
},
};
export default nextConfig;
Next.js 15 muestra un aviso parecido con otro texto: habla de outputFileTracingRoot y de «multiple lockfiles».
6. El servidor de desarrollo
6.1 Arrancar y leer la salida
npm run dev
▲ Next.js 16.4.3 (Turbopack)
- Local: http://127.0.0.1:5501
- Network: http://127.0.0.1:5501
✓ Ready in 8.7s
✓ Running next.config.ts took 1471ms
- Cache Components enabled
- Partial Prefetching enabled
○ Compiling / ...
GET / 200 in 15.6s (next.js: 8.4s, application-code: 7.2s)
(Las salidas de este módulo se obtuvieron en los puertos del laboratorio, 5501 para Next y 3401 para la API. En tu equipo verás 3001 y 3000.)
Ready no significa que la aplicación esté compilada: significa que el servidor escucha. next dev compila cada ruta la primera vez que la pides (○ Compiling / ...). Por eso la primera visita a una página es lenta y las siguientes son rápidas. Es una decisión deliberada: en un proyecto con cien rutas, compilarlas todas al arrancar te haría esperar por páginas que quizá no vas a abrir.
💡 Consejo: La primera vez que abras cada página en desarrollo tardará varios segundos. No juzgues el rendimiento de Next por el modo desarrollo: para medir, compila con
next buildy arranca connext start.
6.2 Fast Refresh
Fast Refresh actualiza el navegador al guardar sin recargar la página entera y, si puede, sin perder el estado de React. Para comprobarlo se ha preparado una portada con un contador (componente cliente) dentro de una página (componente de servidor):
// app/contador.tsx
"use client";
import { useState } from "react";
export function Contador() {
const [clics, setClics] = useState(0);
function sumar() {
const nuevo = clics + 1;
setClics(nuevo);
console.log("Clics:", nuevo);
if (nuevo === 3) console.warn("Ya van tres clics");
}
return (
<button
onClick={sumar}
className="w-fit rounded-lg bg-indigo-700 px-4 py-2 font-medium text-white"
>
Clics: {clics}
</button>
);
}
// app/page.tsx
import Link from "next/link";
import { Contador } from "./contador";
export default function Inicio() {
return (
<main className="mx-auto flex max-w-2xl flex-1 flex-col gap-6 p-12">
<h1 className="text-3xl font-semibold text-indigo-900">EMPRESA</h1>
<p className="text-zinc-600">
Primer proyecto del curso. Edita <code>app/page.tsx</code> y guarda:
el navegador se actualiza sin perder el estado del contador.
</p>
<Contador />
<nav className="flex gap-4">
<Link className="text-indigo-700 underline" href="/empleados/100">
Empleado 100
</Link>
<Link className="text-indigo-700 underline" href="/acerca">
Acerca de
</Link>
</nav>
</main>
);
}
La directiva "use client" marca Contador como componente cliente, el único tipo de componente que puede tener estado y manejar eventos. Lo verás en el módulo 05.
La prueba, automatizada con Playwright: pulsar dos veces el botón, cambiar en el fichero >EMPRESA< por >EMPRESA S.A.<, esperar a que el título cambie en el navegador y leer el botón. El resultado:
Tras editar el título: Clics: 2
El título se actualizó y el contador siguió en 2. Next volvió a ejecutar el Server Component en el servidor, envió el nuevo resultado y React lo mezcló con el árbol que ya había en el navegador, conservando el estado del componente cliente.

Figura 4. La portada de EMPRESA tras pulsar tres veces el botón. Abajo a la izquierda, el indicador avisa de que Next está compilando: es la precarga de los enlaces, que compila las rutas de destino.
Fast Refresh pierde el estado en algunos casos, y conviene saberlo para no pensar que algo va mal:
- Si el fichero editado exporta algo que no es un componente de React (por ejemplo, una constante usada fuera), React no puede garantizar que el estado sigue siendo válido y vuelve a montar.
- Si cambias los argumentos de un hook de forma que el orden de los hooks cambia.
- Si hay un error de sintaxis: cuando lo arreglas, la página se recupera.
- Si añades
// @refresh reseten el fichero, fuerzas que se vuelva a montar en cada edición.
6.3 El overlay de errores y el indicador
En desarrollo, Next muestra los errores dentro de la propia página. Para verlo, se añadió a propósito un error en el manejador del botón:
function sumar() {
const nuevo = clics + 1;
if (nuevo === 3) throw new Error("El contador no admite más de tres clics");
// ...
}
Un error dentro de un manejador de eventos no rompe el renderizado, así que Next no tapa la página. Solo cambia el indicador de la esquina, que pasa a rojo y muestra el número de incidencias:

Figura 5. El indicador avisa de un error sin interrumpir la página.
Al pulsar el indicador se abre el overlay con el mensaje, el fichero y la línea exactos, el código alrededor y la pila de llamadas:

Figura 6. El overlay muestra el error con el código fuente original, no el compilado. Arriba a la derecha, la versión de Next marcada como «stale» porque había un parche más reciente.
Detalles del overlay que te serán útiles:
- «Next.js 16.4.3 (stale)»: el indicador de versión te avisa de que existe un parche más nuevo. Es la misma información que da
next info. - Los iconos de arriba a la derecha copian el error (en un formato listo para pegar a un asistente de IA), abren la documentación y abren el fichero en tu editor.
- «Show ignore-listed frames»: por defecto oculta las líneas de la pila que pertenecen a React o a Next, para que veas solo las de tu código.
- Cuando el error ocurre al renderizar (no en un evento), el overlay se abre solo y tapa la página.
El mismo error llega también al terminal, con su posición en el código fuente:
[browser] Uncaught Error: El contador no admite más de tres clics
at sumar (app/contador.tsx:10:28)
at button (<anonymous>)
at Contador (app/contador.tsx:14:3)
at Inicio (app\page.tsx:12:7)
8 | function sumar() {
9 | const nuevo = clics + 1;
> 10 | if (nuevo === 3) throw new Error("El contador no admite más de tres clics");
| ^
11 | setClics(nuevo);
12 | console.log("Clics:", nuevo);
13 | if (nuevo === 3) console.warn("Ya van tres clics");
El indicador de desarrollo se configura con la opción devIndicators de next.config.ts: devIndicators: { position: 'bottom-right' } lo cambia de esquina y devIndicators: false lo oculta (los errores siguen apareciendo).
6.4 Logs del navegador en el terminal 🆕 16.2
🆕 Desde Next.js 16.2. La opción
logging.browserToTerminalreenvía la consola del navegador al terminal denext dev; por defecto reenvía avisos y errores. En Next.js 15.4 y 15.5 existe como opción experimental con otro nombre. Si usas Next.js 15: escribeexperimental: { browserDebugInfoInTerminal: true }ennext.config.ts; con versiones anteriores a la 15.4 tendrás que mirar la consola del navegador.
En el proyecto del módulo se activó el reenvío de todo, no solo de avisos y errores:
const nextConfig: NextConfig = {
logging: {
browserToTerminal: true,
},
};
Valores posibles: 'warn' (avisos y errores, por defecto), 'error' (solo errores), true (todo) y false (nada). Tras pulsar tres veces el botón, el terminal de next dev mostró:
[browser] Download the React DevTools for a better development experience: https://react.dev/link/react-devtools
[browser] Clics: 1 (app/contador.tsx:11:13)
[browser] Clics: 2 (app/contador.tsx:11:13)
[browser] Clics: 3 (app/contador.tsx:11:13)
[browser] Ya van tres clics (app/contador.tsx:12:30)
Cada mensaje lleva el prefijo [browser] y la posición del console.log en tu código fuente. Es cómodo: tienes los logs del servidor y del cliente en el mismo sitio. Y es justo lo que necesita un asistente de IA, que lee el terminal pero no ve tu navegador.
📘 Concepto: Un
console.logen un Server Component se imprime en el terminal, porque ese código se ejecuta en Node. Unconsole.logen un Client Component se imprime en la consola del navegador (y, con esta opción, también en el terminal con el prefijo[browser]). Si no encuentras un log, pregúntate primero dónde se ejecuta ese código.
6.5 La carpeta .next/dev 🔄 16.0
🔄 Cambia en Next.js 16.0.
next devescribe ahora en.next/dev, separado de la salida denext builden.next; en Next.js 15 ambos comparten.next. Si usas Next.js 15: para el servidor de desarrollo antes de ejecutarnext builden el mismo proyecto, porque los dos escriben en la misma carpeta y pueden estropearse mutuamente los ficheros.
Figura 7. Desde Next.js 16, desarrollo y producción no comparten carpeta.
Con el servidor de desarrollo en marcha y una compilación de producción hecha antes, .next contenía esto:
.next:
app-path-routes-manifest.json BUILD_ID build-manifest.json
build cache dev
diagnostics export-marker.json fallback-build-manifest.json
images-manifest.json next-server.js.nft.json prerender-manifest.json
required-server-files.json routes-manifest.json server
static trace turbopack
types
.next/dev:
build build-manifest.json cache fallback-build-manifest.json
lock logs package.json prerender-manifest.json
routes-manifest.json server static trace
types
Dentro de .next/dev hay dos ficheros interesantes:
.next/dev/lock guarda el proceso, el puerto y la URL del servidor de desarrollo:
{"pid":45136,"port":5501,"hostname":"127.0.0.1","appUrl":"http://127.0.0.1:5501","startedAt":1793618204660}
Gracias a él, si intentas arrancar un segundo next dev en el mismo proyecto, Next no lo permite y te dice dónde está el que ya funciona:
⨯ Another next dev server is already running.
- Local: http://127.0.0.1:5501
- PID: 6804
- Dir: C:\proyectos\mi-app-16
- Log: .next\dev\logs\next-development.log
Es un error muy habitual: dejas un terminal olvidado con el servidor abierto, abres otro y lanzas npm run dev. Antes acababas con dos servidores en puertos distintos y cambios que «no se veían» porque mirabas el que no era. Ahora Next te lo dice.
.next/dev/logs/next-development.log es el registro completo de la sesión de desarrollo: lo que sale por el terminal, incluidos los logs del navegador. Lo usan las herramientas de agentes (apartado 10).
En Next.js 15.5, la carpeta .next del proyecto en desarrollo no tiene subcarpeta dev ni fichero lock:
app-build-manifest.json build build-manifest.json cache fallback-build-manifest.json
package.json postcss.js postcss.js.map prerender-manifest.json routes-manifest.json
server static trace types
7. Compilar para producción: next build y next start
7.1 Una compilación limpia
next build compila todas las rutas, comprueba los tipos, prerenderiza lo que puede y deja el resultado en .next. La compilación de la plantilla 16.4 recién creada:
> mi-app-16@0.1.0 build
> next build
▲ Next.js 16.4.3 (Turbopack)
✓ Running next.config.ts took 1848ms
- Cache Components enabled
- Partial Prefetching enabled
Creating an optimized production build ...
✓ Compiled successfully in 36.1s
Running TypeScript ...
Finished TypeScript in 79s ...
Collecting page data using 5 workers ...
Finalizing page optimization ...
Route (app)
┌ ○ /
└ ○ /_not-found
○ (Static) prerendered as static content
Las fases, en orden:
- Cargar la configuración y avisar de las opciones activas.
- Compilar con Turbopack (
Compiled successfully). - Comprobar tipos (
Running TypeScript). Si hay errores de TypeScript, la compilación falla aquí. Next.js 16.4 ejecuta eltscinstalado en tu proyecto. - Recoger datos de las páginas y prerenderizar las rutas que se pueden generar por adelantado.
- Tabla de rutas, con un símbolo para cada una.
/_not-found es la página 404 que Next genera si no defines la tuya. Los símbolos de la tabla son la información más valiosa de la compilación:
| Símbolo | Significado |
|---|---|
○ (Static) | HTML generado al compilar; se sirve igual a todo el mundo |
◐ (Partial Prerender) | Armazón HTML generado al compilar, con partes dinámicas que llegan en streaming |
ƒ (Dynamic) | Se renderiza en cada petición |
Los modelos de renderizado y lo que significa cada símbolo los verás a fondo en el módulo 02.
Después, next start sirve el resultado:
npm run build
npm start -- -p 3001
▲ Next.js 16.4.3
- Local: http://127.0.0.1:5501
- Network: http://127.0.0.1:5501
✓ Ready in 1219ms
Nota que ya no dice (Turbopack): next start no compila nada, solo sirve.
7.2 La tabla de rutas sin tamaños 🔄 16.0
🔄 Cambia en Next.js 16.0.
next buildya no muestra el tamaño de cada ruta ni el «First Load JS», y tampoco ejecuta ESLint durante la compilación. Si usas Next.js 15: verás esas columnas y la fase «Linting and checking validity of types»; en Next.js 16 usanext analyze(onext build --analyze) para conocer los tamaños y ejecutanpm run lintpor separado.
La misma página de bienvenida compilada con Next.js 15.5.28:
Linting and checking validity of types ...
Collecting page data ...
✓ Generating static pages (5/5)
Finalizing page optimization ...
Collecting build traces ...
Route (app) Size First Load JS
┌ ○ / 5.47 kB 119 kB
└ ○ /_not-found 0 B 114 kB
+ First Load JS shared by all 118 kB
├ chunks/3e9f5793320a7fd0.js 21.1 kB
├ chunks/ba5bf9a0ee17d57e.js 75.4 kB
└ other shared chunks (total) 21.5 kB
○ (Static) prerendered as static content
El equipo de Next quitó los tamaños porque con los Server Components y el streaming esas cifras ya no reflejaban bien lo que descarga el usuario. El analizador da una información mucho más útil: qué módulo ocupa cuánto y por qué está incluido. Se verá en el módulo 20.
7.3 Tu primer error de compilación con Cache Components 🔄 16.0
🔄 Cambia en Next.js 16.0. Con
cacheComponentsactivado,next buildfalla si una página lee datos de la petición (params,searchParams,cookies()…) o datos sin caché fuera de un<Suspense>. En Next.js 15 esa misma página compila sin error como ruta dinámica (ƒ). Si usas Next.js 15: el código sin<Suspense>funciona tal cual; añadir<Suspense>es igualmente buena práctica, porque permite enviar antes el resto de la página.
Esta es la página más sencilla posible para una ruta dinámica, app/empleados/[id]/page.tsx:
export default async function PaginaEmpleado(props: PageProps<"/empleados/[id]">) {
const { id } = await props.params;
return (
<main className="p-12">
<h1 className="text-2xl font-semibold">Empleado {id}</h1>
</main>
);
}
En Next.js 16 con la configuración de la plantilla, next build se detiene:
Error: Route "/empleados/[id]": Next.js encountered uncached or runtime data during prerendering.
`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.
Ways to fix this:
- [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
- [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` (does not apply to `connection()`)
- [block] Set `export const instant = false` to allow a blocking route
Learn more: https://nextjs.org/docs/messages/blocking-prerender-dynamic
at body (<anonymous>)
at html (<anonymous>)
To get a more detailed stack trace and pinpoint the issue, try one of the following:
- Start the app in development mode by running `next dev`, then open "/empleados/[id]" in your browser to investigate the error.
- Rerun the production build with `next build --debug-prerender` to generate better stack traces.
Error occurred prerendering page "/empleados/[id]". Read more: https://nextjs.org/docs/messages/prerender-error
Export encountered an error on /empleados/[id]/page: /empleados/[id], exiting the build.
⨯ Next.js build worker exited with code: 1 and signal: null
El mensaje es largo pero muy claro. Next intenta prerenderizar todas las páginas al compilar. El valor de id no se conoce hasta que llega una petición, así que la página no puede completarse al compilar. Next se niega a decidir por ti y te ofrece tres salidas: mostrar un contenido provisional y enviar el resto después ([stream]), guardar los datos en caché ([cache]) o aceptar una página que bloquea hasta tenerlo todo ([block]).
La solución habitual es la primera: mover la lectura de params a un componente hijo y envolverlo en <Suspense>:
import { Suspense } from "react";
async function Detalle({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
return <h1 className="text-2xl font-semibold">Empleado {id}</h1>;
}
export default function PaginaEmpleado(props: PageProps<"/empleados/[id]">) {
return (
<main className="p-12">
<Suspense fallback={<p>Cargando empleado...</p>}>
<Detalle params={props.params} />
</Suspense>
</main>
);
}
La página ya no es async: pasa la promesa params sin esperarla, y es Detalle quien la espera dentro del <Suspense>. Ahora la compilación termina:
Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /acerca
└ /empleados/[id]
└ ◐ /empleados/[id]
○ (Static) prerendered as static content
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
/empleados/[id] es ahora ◐: el <main> y el texto «Cargando empleado…» se generan al compilar y se sirven al instante; el nombre del empleado llega en streaming con cada petición.
La versión sin <Suspense> compilada con Next.js 15.5.28 (con typedRoutes, apartado 8.4) no da error:
Route (app) Size First Load JS
┌ ○ / 5.47 kB 120 kB
├ ○ /_not-found 0 B 115 kB
└ ƒ /empleados/[id] 3.42 kB 118 kB
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
💡 Consejo: No te asustes si en tus primeros proyectos con Next 16 la compilación falla por esto. Es la forma que tiene Cache Components de obligarte a pensar qué parte de cada página es estática y cuál dinámica. En el módulo 02 entenderás qué significa
◐, y en el módulo 09 la opción[cache].
7.4 Desarrollo y producción no son lo mismo
Hay comportamientos que solo aparecen en uno de los dos modos. Tenlo presente cuando algo «funciona en mi equipo»:
next dev | next build + next start | |
|---|---|---|
| Compilación | Por ruta, al pedirla | Todas, antes de arrancar |
| Comprobación de tipos | Solo en el editor | Obligatoria: falla la compilación |
| Prerenderizado | No: todo se renderiza en cada petición | Sí: las rutas ○ se generan una vez |
| Errores | Overlay con código fuente | Página de error genérica, sin detalles |
| React | Modo desarrollo (más comprobaciones, más lento) | Modo producción |
| Errores de Cache Components | En el overlay al abrir la página | Detienen la compilación |
Por eso conviene ejecutar npm run build de vez en cuando mientras desarrollas, no solo al final. Y gracias a .next/dev, en Next 16 puedes hacerlo sin parar el servidor de desarrollo.
8. TypeScript
8.1 El tsconfig.json de la plantilla
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}
Las opciones que importan para entender Next:
noEmit: true: TypeScript no genera JavaScript. Solo comprueba tipos. Quien transforma tu TypeScript en JavaScript es Turbopack (con SWC, un compilador escrito en Rust), que simplemente quita los tipos sin comprobarlos. Por eso la página puede funcionar ennext devaunque tenga errores de tipos: los ves en el editor, pero no bloquean nada hastanext build.isolatedModules: true: exige que cada fichero se pueda compilar por separado, que es como trabaja SWC. Por ejemplo, obliga a escribirimport typeoexport typecuando reexportas solo tipos.moduleResolution: "bundler": resuelve losimportcomo lo hace un bundler moderno (respeta el campoexportsde los paquetes y no exige extensiones).strict: true: activa todas las comprobaciones estrictas. No lo desactives: la mitad del valor de TypeScript está aquí.plugins: [{ "name": "next" }]: el plugin del editor del apartado 1.3.paths: el alias@/*. Con él,import { formatear } from "@/lib/formato"funciona desde cualquier carpeta.include: además de tu código, incluye los tipos que genera Next en.next/types(producción ytypegen) y en.next/dev/types(desarrollo).
La plantilla de Next.js 15 usa "jsx": "preserve" y no incluye .next/dev/types/**/*.ts. No toques estos valores: si Next los necesita distintos, los corrige él mismo al arrancar y te avisa.
8.2 next-env.d.ts
Es un fichero que Next escribe y reescribe. Comparando los dos proyectos:
// Next.js 16.4.3
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
// Next.js 15.5.28
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Su trabajo es triple: cargar los tipos de Next, declarar los tipos de las importaciones que no son código (import logo from "./logo.png" devuelve un objeto con src, width y height) y traer los tipos generados de tus rutas. En desarrollo, la ruta de los imports cambia a .next/dev/types: es una de las razones por las que no se sube a Git.
8.3 Tipos generados de las rutas: next typegen, PageProps y LayoutProps
Next recorre la carpeta app/ y genera un fichero de tipos con todas tus rutas. Lo hace en next dev, en next build y con un comando específico, next typegen, que no compila la aplicación:
npx next typegen
Generating route types...
✓ Types generated successfully
Con las rutas del proyecto del módulo (/, /acerca y /empleados/[id]), el fichero .next/types/routes.d.ts empieza así:
// This file is generated automatically by Next.js
// Do not edit this file manually
type AppRoutes = "/" | "/acerca" | "/empleados/[id]"
type PageRoutes = never
type LayoutRoutes = "/"
type RedirectRoutes = never
type RewriteRoutes = never
type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRoutes
interface ParamMap {
"/": {}
"/acerca": {}
"/empleados/[id]": { "id": string; }
}
Y más abajo declara los tipos globales PageProps, LayoutProps y RouteContext:
declare global {
interface PageProps<AppRoute extends AppRoutes> {
params: Promise<ParamMap[AppRoute]>
// ... también searchParams
}
}
Figura 8. Next convierte la estructura de carpetas en tipos de TypeScript.
Esto te permite tipar una página sin escribir a mano la forma de sus parámetros:
export default async function PaginaEmpleado(props: PageProps<"/empleados/[id]">) {
const { id } = await props.params; // id: string
// ...
}
Y si te equivocas en la ruta, TypeScript lo detecta. Con PageProps<"/empleado/[id]"> (falta la «s»), tsc responde:
app/pruebas/page.tsx(3,50): error TS2344: Type '"/empleado/[id]"' does not satisfy the constraint 'AppRoutes'.
PageProps, LayoutProps, RouteContext y next typegen existen desde Next.js 15.5, así que puedes usarlos igual en 15.5.28. Se comprobó compilando la misma página con PageProps en el laboratorio de Next.js 15.
💡 Consejo: Al crear una ruta nueva, el tipo
PageProps<"/ruta/nueva">no existe hasta que Next regenera los tipos. Connext deven marcha ocurre solo al guardar. Si no tienes el servidor arrancado y el editor marca error, ejecutanpx next typegen.
next typegen es especialmente útil en integración continua: puedes comprobar los tipos de todo el proyecto sin compilarlo, que es mucho más rápido.
{
"scripts": {
"typecheck": "next typegen && tsc --noEmit"
}
}
8.4 Rutas tipadas con typedRoutes
Con typedRoutes: true en next.config.ts, Next genera también los tipos de los href válidos, y TypeScript comprueba cada <Link> y cada router.push():
const nextConfig: NextConfig = {
typedRoutes: true,
};
Un enlace mal escrito:
<Link href="/acercaa">Mal escrito</Link>
produce este error, con sugerencia incluida:
app/pruebas/page.tsx(7,13): error TS2820: Type '"/acercaa"' is not assignable to type 'UrlObject | RouteImpl<"/acercaa">'. Did you mean '"/acerca"'?
Y como next build comprueba los tipos, la compilación también falla:
Running TypeScript ...
app/pruebas/page.tsx(3,50): error TS2344: Type '"/empleado/[id]"' does not satisfy the constraint 'AppRoutes'.
app/pruebas/page.tsx(7,13): error TS2820: Type '"/acercaa"' is not assignable to type 'UrlObject | RouteImpl<"/acercaa">'. Did you mean '"/acerca"'?
Failed to type check.
Las rutas dinámicas también se comprueban: href="/empleados/100" es válido porque encaja con /empleados/[id]. Si construyes el href con una variable que no es literal, tendrás que indicar el tipo con as Route (importado de next). typedRoutes es estable desde Next.js 15.5: en el laboratorio de 15.5.28 el mismo enlace da el error TS2322 (sin la sugerencia «Did you mean»).
En este curso activaremos typedRoutes en el proyecto EMPRESA: cuesta cero y elimina una clase entera de errores (enlaces rotos al renombrar una carpeta).
8.5 Errores de tipos: editor, tsc y build
Recapitulando, los errores de tipos aparecen en tres sitios:
- En el editor, mientras escribes. Es lo más cómodo, si has activado la versión del proyecto (apartado 1.3).
- Con
npx tsc --noEmit(o el scripttypecheck), que revisa todo el proyecto. - En
next build, que falla si hay alguno.
next dev no comprueba tipos: la página funciona aunque tenga errores de tipos. Es una decisión de rendimiento (comprobar tipos es lento), pero significa que no puedes fiarte de que «si se ve, compila». Existe una opción typescript.ignoreBuildErrors: true que salta la comprobación en build; no la uses salvo emergencia, porque estarás desplegando código que sabes que tiene errores.
9. ESLint con flat config 🔄 16.0
🔄 Cambia en Next.js 16.0. El comando
next lintse ha eliminado,next buildya no ejecuta ESLint y la opcióneslintdenext.configdeja de tener efecto. En Next.js 15.5,next lintsigue funcionando, aunque marcado como obsoleto, ynext buildejecuta el linter. Si usas Next.js 15: puedes seguir connext lint, pero es mejor migrar ya al CLI de ESLint connpx @next/codemod@canary next-lint-to-eslint-cli .; así el paso a 16 no te obligará a tocar nada.
9.1 La configuración
La plantilla genera eslint.config.mjs en formato flat config, el formato de ESLint 9 y 10:
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
Un flat config es un array de objetos de configuración que se aplican en orden; los posteriores pueden sobrescribir a los anteriores. Aquí:
eslint-config-next/core-web-vitalsincluye las reglas de@next/eslint-plugin-next,eslint-plugin-reactyeslint-plugin-react-hooks, y convierte en errores las que afectan a las Core Web Vitals (rendimiento percibido).eslint-config-next/typescriptañade las reglas detypescript-eslint.globalIgnoresindica qué carpetas no se revisan: la salida de compilación ynext-env.d.ts.
9.2 Ejecutarlo
npm run lint # ejecuta "eslint" sobre el proyecto
npx eslint --fix . # corrige automáticamente lo que pueda
Si no hay problemas, ESLint no escribe nada. Para ver cómo avisa, se añadió una imagen con <img> en lugar de <Image>:
<img src="/next.svg" alt="Logo" />
C:\proyectos\empresa\app\pruebas\page.tsx
8:7 warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
✖ 1 problem (0 errors, 1 warning)
Cada aviso trae el fichero, la línea y columna, el nivel (warning o error), la explicación, un enlace y el nombre de la regla (@next/next/no-img-element). Algunas reglas del plugin de Next que te encontrarás:
| Regla | Qué evita |
|---|---|
@next/next/no-img-element | <img> en lugar de next/image |
@next/next/no-html-link-for-pages | <a> para navegar entre páginas internas (en lugar de <Link>) |
@next/next/no-async-client-component | Componentes cliente declarados como async |
@next/next/no-head-element | Usar <head> a mano |
@next/next/no-sync-scripts | <script> síncronos que bloquean la carga |
@next/next/inline-script-id | next/script con código en línea y sin id |
9.3 next lint ya no existe
En Next.js 16, next lint no da un error claro: interpreta lint como el nombre de una carpeta (todos los comandos de next aceptan un directorio) y falla:
Invalid project directory provided, no such directory: C:\proyectos\empresa\lint
Si ves este mensaje en un proyecto antiguo, cambia el script "lint": "next lint" por "lint": "eslint". En Next.js 15.5, en cambio, next lint avisa de su retirada:
`next lint` is deprecated and will be removed in Next.js 16.
For new projects, use create-next-app to choose your preferred linter.
For existing projects, migrate to the ESLint CLI:
npx @next/codemod@canary next-lint-to-eslint-cli .
9.4 ESLint 9, ESLint 10 o Biome
eslint-config-next 16.4 admite ESLint 9 y 10. ESLint 10 exige Node ^20.19.0, ^22.13.0 o >=24. Algunos plugins que incluye todavía no declaran ESLint 10 entre sus dependencias compatibles, así que con ESLint 10 npm puede mostrar avisos de peer dependencies. La plantilla se queda en ESLint 9 por prudencia, de ahí el aviso de «no longer supported» al instalar. Ambas opciones funcionan.
Si prefieres Biome, elígelo al crear el proyecto (--biome). Biome es un linter y formateador en un solo binario escrito en Rust: mucho más rápido, con menos reglas, y con soporte de los dominios de Next y React. En este curso se usa ESLint porque es lo que encontrarás en la mayoría de proyectos.
💡 Consejo: ESLint y Prettier no compiten: ESLint busca errores y malas prácticas, Prettier da formato al código. Si añades Prettier, instala también
eslint-config-prettiery ponlo al final del array de configuración para que desactive las reglas de estilo de ESLint que chocarían con él.
10. Herramientas para asistentes de IA
Los asistentes de programación (Claude Code, Codex, Cursor, GitHub Copilot) se entrenan con código que siempre va por detrás de la última versión de Next. El resultado típico: te proponen middleware.ts en lugar de proxy.ts, params síncronos o next lint. Next 16 trae varias piezas para reducir ese problema. Aunque no uses asistentes, conviene saber qué son esos ficheros que aparecen en tu proyecto.
10.1 AGENTS.md y la documentación local 🆕 16.2
🆕 Desde Next.js 16.2. El paquete
nextincluye su documentación ennode_modules/next/dist/docs/ycreate-next-appgeneraAGENTS.md; desde la 16.3,next devmantiene ese fichero actualizado (opciónagentRules). No existe en Next.js 15. Si usas Next.js 15: ejecutanpx @next/codemod@canary agents-md, que descarga una copia de la documentación de tu versión en.next-docs/y crea el índice enAGENTS.md.
El AGENTS.md que genera la plantilla contiene un único bloque gestionado por Next:
<!-- BEGIN:nextjs-agent-rules -->
## This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
<!-- END:nextjs-agent-rules -->
Le dice al asistente que no se fíe de lo que «sabe» y que lea la documentación instalada con tu versión. Esa documentación es la misma que la web oficial, en Markdown, con la misma estructura de carpetas:
node_modules/next/dist/docs/
├── 01-app/
│ ├── 01-getting-started/
│ ├── 02-guides/
│ └── 03-api-reference/
├── 02-pages/
├── 03-architecture/
└── index.md
Para ti también es útil. Si dudas de cómo funciona una opción en tu versión exacta, busca en esa carpeta antes que en internet: lo que pone ahí es lo que hace tu Next. Por ejemplo, la referencia de la CLI está en 01-app/03-api-reference/06-cli/next.md y cada opción de configuración en 01-app/03-api-reference/05-config/01-next-config-js/. Al actualizar Next, la documentación se actualiza con él.
Puedes escribir tus propias instrucciones en AGENTS.md, fuera del bloque marcado: Next solo modifica lo que está entre BEGIN y END. Por ejemplo, para el proyecto EMPRESA:
## Convenciones del proyecto EMPRESA
- Textos de la interfaz, identificadores y comentarios en español.
- La API de datos está en http://localhost:3000/api; Next se arranca en el puerto 3001.
- Usa `PageProps` y `LayoutProps` para tipar páginas y layouts.
Cuando next dev detecta que lo ha lanzado un asistente de IA, crea o actualiza el bloque y lo dice en la consola. Al arrancar el proyecto del módulo desde un asistente apareció esta línea:
✓ Generated AGENTS.md for AI agents. Set `agentRules: false` in next.config to disable.
Si no quieres que Next toque AGENTS.md, pon agentRules: false en next.config.ts: desde la 16.4, el siguiente next dev elimina el bloque (y el fichero, si no tenía nada más). Para que create-next-app no lo cree, usa --no-agents-md.
📘 Concepto: Hay asistentes que leen
CLAUDE.mden lugar deAGENTS.md. Si el tuyo es uno de ellos, crea unCLAUDE.mdcon una línea que haga referencia aAGENTS.md(por ejemplo,@AGENTS.md) para no mantener dos ficheros.create-next-app16.4.3 solo escribeAGENTS.md.
10.2 Agent feedback (experimental)
La opción experimental.agentFeedback añade a AGENTS.md un segundo bloque que pide al asistente que tome nota (anonimizada) de los fallos de Next que encuentre mientras trabaja y, al final, abra en tu navegador un formulario de informe para que tú lo revises y decidas si se envía. Está desactivada por defecto, necesita la telemetría activa y no funciona en integración continua. Como se vio en el apartado 2.2, la activa el menú interactivo «recommended defaults», pero no --yes.
10.3 El servidor MCP de desarrollo 🆕 16.0
🆕 Desde Next.js 16.0.
next devexpone un servidor MCP en/_next/mcpcon información del proyecto en marcha. En Next.js 15 esa ruta no existe y responde 404. Si usas Next.js 15: no hay alternativa equivalente; el asistente tendrá que leer el terminal y ejecutarnext buildpara encontrar errores.
MCP (Model Context Protocol) es un estándar abierto para que los asistentes de IA llamen a herramientas externas. El servidor de desarrollo de Next 16 publica un endpoint MCP con herramientas que dan al asistente la visión del propio framework: qué rutas hay, qué errores tiene la página abierta, dónde está el log o si una ruta compila.
Para comprobar que existe y qué ofrece, basta con una petición JSON-RPC con curl mientras next dev está en marcha:
curl -s -X POST http://localhost:3001/_next/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
La respuesta (filtrada para mostrar solo los nombres) lista nueve herramientas:
get_project_metadata get_errors get_page_metadata get_logs get_server_action_by_id
get_routes get_request_insights get_compilation_issues compile_route
Y llamar a una de ellas, por ejemplo get_routes:
curl -s -X POST http://localhost:3001/_next/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_routes","arguments":{}}}'
event: message
data: {"result":{"content":[{"type":"text","text":"{\n \"appRouter\": [\n \"/\",\n \"/acerca\",\n \"/empleados/[id]\",\n \"/favicon.ico\"\n ]\n}"}]},"jsonrpc":"2.0","id":2}
| Herramienta | Qué devuelve |
|---|---|
get_errors | Errores actuales: configuración, compilación y errores del navegador con su pila |
get_logs | La ruta del fichero de log de desarrollo |
get_routes | Todas las rutas, agrupadas por router |
get_page_metadata | Qué ficheros contribuyen a la página abierta en el navegador |
get_project_metadata | Ruta del proyecto y URL del servidor |
get_compilation_issues | Errores de compilación de todas las rutas, sin abrir el navegador |
compile_route | Compila una ruta concreta y devuelve sus errores |
get_server_action_by_id | El fichero y la función de una Server Action a partir de su identificador |
get_request_insights | Cronología de peticiones y caché (requiere experimental.requestInsights) |
Para conectar tu asistente, se usa el paquete next-devtools-mcp, que descubre los servidores de desarrollo en marcha y reenvía las llamadas. Se declara en .mcp.json en la raíz del proyecto:
{
"mcpServers": {
"next-devtools": {
"command": "npx",
"args": ["-y", "next-devtools-mcp@latest"]
}
}
}
El endpoint está activo por defecto en desarrollo y nunca en producción. Para desactivarlo: experimental.mcpServer: false en next.config.ts.
⚠️ Importante: Recuerda que
next devescucha por defecto en todas las interfaces (0.0.0.0). Cualquiera en tu red local puede abrir tu aplicación de desarrollo y llamar a/_next/mcp. En redes públicas (una cafetería, un congreso), arranca con-H 127.0.0.1.
11. Caso práctico: el esqueleto de EMPRESA
Vas a montar el proyecto que crecerá durante todo el curso. Al terminar tendrás: Next en el puerto 3001, la API en el 3000, textos en español, rutas tipadas, logs del navegador en el terminal y una página que comprueba la conexión con la API.
Paso 1: crear el proyecto
npx create-next-app@latest empresa --yes
cd empresa
Paso 2: fijar los puertos y añadir scripts
Edita los scripts de package.json:
{
"scripts": {
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start -p 3001",
"lint": "eslint",
"typecheck": "next typegen && tsc --noEmit"
}
}
Paso 3: configurar Next
next.config.ts con typedRoutes y el reenvío completo de logs:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
typedRoutes: true,
logging: {
browserToTerminal: true,
},
turbopack: {
rules: {
"*.css": {
loaders: ["@tailwindcss/turbopack"],
as: "*.css",
},
},
},
};
export default nextConfig;
Paso 4: la dirección de la API en una variable de entorno
Crea .env.local en la raíz (ya está ignorado por Git):
API_URL=http://localhost:3000/api
Las variables de .env.local se cargan al arrancar Next y están disponibles en el servidor como process.env.API_URL. No llevan el prefijo NEXT_PUBLIC_, así que no llegan al navegador; aquí no hace falta, porque la API se consultará desde el servidor. Si cambias .env.local, Next lo recarga; aun así, si algo no cuadra, reinicia next dev. Las variables de entorno se ven a fondo en el módulo 13.
Paso 5: el layout raíz en español
En app/layout.tsx, cambia los metadatos y el idioma:
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "EMPRESA",
description: "Aplicación de gestión del Curso de Next.js",
};
export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html
lang="es"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
}
Paso 6: las páginas
Sustituye app/page.tsx por la portada del apartado 6.2 y crea app/contador.tsx con el contador del mismo apartado. Añade app/acerca/page.tsx:
export default function Acerca() {
return (
<main className="p-12">
<h1 className="text-2xl font-semibold">Acerca de EMPRESA</h1>
<p>Aplicación de ejemplo del Curso de Next.js.</p>
</main>
);
}
Y app/empleados/[id]/page.tsx, la versión con <Suspense> del apartado 7.3.
Paso 7: la página de estado de la API
app/estado-api/page.tsx consulta la ruta /api/estadisticas y muestra los totales. Si la API no responde, lo dice en lugar de romper la página:
import { Suspense } from "react";
const API_URL = process.env.API_URL ?? "http://localhost:3000/api";
type Estadisticas = {
empleados: number;
departamentos: number;
productos: number;
pedidos: number;
};
// Devuelve los datos o un texto con el problema. Aquí no se construye JSX.
async function obtenerEstadisticas(): Promise<Estadisticas | string> {
try {
const respuesta = await fetch(`${API_URL}/estadisticas`);
if (!respuesta.ok) return `La API responde con ${respuesta.status}`;
return await respuesta.json();
} catch {
return `No se puede conectar con ${API_URL}. ¿Está arrancada la API?`;
}
}
async function EstadoApi() {
const resultado = await obtenerEstadisticas();
if (typeof resultado === "string") {
return <p className="text-red-700">{resultado}</p>;
}
return (
<ul className="list-disc pl-6 text-zinc-700">
<li>Empleados: {resultado.empleados}</li>
<li>Departamentos: {resultado.departamentos}</li>
<li>Productos: {resultado.productos}</li>
<li>Pedidos: {resultado.pedidos}</li>
</ul>
);
}
export default function PaginaEstadoApi() {
return (
<main className="mx-auto flex max-w-2xl flex-col gap-4 p-12">
<h1 className="text-2xl font-semibold text-indigo-900">Estado de la API EMPRESA</h1>
<p className="text-sm text-zinc-500">Origen de datos: {API_URL}</p>
<Suspense fallback={<p>Consultando la API...</p>}>
<EstadoApi />
</Suspense>
</main>
);
}
¿Por qué separar obtenerEstadisticas de EstadoApi, en lugar de devolver el JSX directamente desde el try y el catch? Porque así fue la primera versión, y ESLint la rechazó con ocho errores como este:
C:\proyectos\empresa\app\estado-api\page.tsx
16:14 error Error: Avoid constructing JSX within try/catch
React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary.
...
✖ 8 problems (8 errors, 0 warnings)
La regla react-hooks/error-boundaries, incluida en la configuración de la plantilla, tiene razón: crear JSX (<p>...</p>) no renderiza nada en ese momento, solo describe qué renderizar. Si al renderizarse falla un componente hijo, el try/catch ya ha terminado y no lo captura. Da una falsa sensación de seguridad. El patrón correcto es el de arriba: el try/catch envuelve solo lo que puede fallar de verdad (la red y el JSON) y devuelve datos; el componente decide qué pintar fuera del try. Para los errores de renderizado están los error boundaries y error.tsx (módulo 14).
Un detalle más: esa primera versión, con sus ocho errores de ESLint, compilaba sin problemas con next build, porque Next 16 ya no ejecuta el linter al compilar (apartado 7.2). Si no ejecutas npm run lint, nadie te avisa.
El fetch está dentro de un componente envuelto en <Suspense>, por la misma razón que en el apartado 7.3: con Cache Components, un fetch sin caché fuera de <Suspense> detiene la compilación. Así, además, la compilación no necesita la API en marcha: el armazón de la página (título, origen y «Consultando la API…») se genera al compilar, y la consulta se hace en cada petición. Cómo pedir datos en el servidor se explica en el módulo 08.
Paso 8: comprobar todo
Con la API arrancada en otro terminal (node server.mjs en la carpeta empresa-api):
npm run typecheck
npm run lint
npm run build
npm start
typecheck y lint no escriben nada si todo está bien. La compilación del proyecto de este módulo terminó así:
Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /acerca
├ /empleados/[id]
│ └ ◐ /empleados/[id]
└ ◐ /estado-api
○ (Static) prerendered as static content
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content
La portada y «Acerca de» son estáticas; la ficha de empleado y el estado de la API tienen un armazón estático y una parte dinámica. Abre http://localhost:3001/estado-api:

Figura 9. La página de estado con la API en marcha. La captura se hizo en el laboratorio del curso, con la API en el puerto 3401; en tu equipo verás http://localhost:3000/api.
Si paras la API y recargas, la página sigue funcionando y el HTML contiene el mensaje de error controlado:
No se puede conectar con http://127.0.0.1:3401/api. ¿Está arrancada la API?
Con la API en marcha, en el código fuente de la página (Ctrl+U) verás Empleados: <!-- -->24. Ese comentario vacío lo pone React entre dos textos que en JSX eran partes distintas (el literal Empleados: y la expresión {resultado.empleados}), para saber dónde termina cada uno al hidratar. El mensaje de error no lo lleva porque se construye como una sola cadena.
Paso 9: Git
create-next-app ya inicializó el repositorio. Haz el primer commit con todo, incluido AGENTS.md:
git add .
git commit -m "Esqueleto del proyecto EMPRESA"
Comprueba con git status que no aparecen .next/, node_modules/, .env.local ni next-env.d.ts.
12. Errores frecuentes
«You are using Node.js X. For Next.js, Node.js version “>=20.9.0” is required.»
Tu Node es demasiado antiguo para Next 16. Actualiza Node a una LTS (o cambia de versión con tu gestor de versiones) y vuelve a ejecutar npm install, porque algunos paquetes compilan binarios para tu versión de Node.
La API o Next no arrancan: puerto ocupado
Next usa el 3000 por defecto, el mismo que la API EMPRESA. Arranca Next con -p 3001 (o fíjalo en el package.json, como en el caso práctico). Si el puerto lo ocupa un proceso olvidado, búscalo y ciérralo: en Windows, netstat -ano | findstr :3001 te da el PID, y taskkill /PID <pid> /F lo termina.
«Another next dev server is already running.»
Tienes otro next dev en marcha para el mismo proyecto. El mensaje te da su URL y su PID: usa ese servidor o ciérralo. No borres .next/dev/lock a mano mientras el proceso exista.
«Next.js ignored package-lock.json in …»
Hay un fichero de bloqueo en una carpeta superior. Si no pertenece a ningún proyecto (típico: C:\Users\tu-usuario\package-lock.json tras un npm install en la carpeta equivocada), bórralo. Si es un monorepo, configura turbopack.root (apartado 5.4).
«Invalid project directory provided, no such directory: …\lint»
Estás ejecutando next lint en Next 16. Cambia el script a "lint": "eslint".
La compilación falla con «uncached or runtime data during prerendering»
Una página lee params, searchParams, cookies(), headers() o hace un fetch sin caché fuera de <Suspense>. Envuelve esa parte en <Suspense> (apartado 7.3) o guárdala en caché con 'use cache' (módulo 09). En desarrollo, abre la página con next dev: el overlay te señala la línea exacta.
La compilación falla con «Failed to type check» aunque en desarrollo todo funcionaba
next dev no comprueba tipos; next build sí. Ejecuta npm run typecheck para ver todos los errores de golpe y corrígelos. No uses ignoreBuildErrors para salir del paso.
PageProps<"/nueva-ruta"> da error en el editor
Los tipos de la ruta todavía no se han generado. Con next dev en marcha, guarda el fichero; sin él, ejecuta npx next typegen. Si sigue fallando, comprueba que la ruta escrita coincide con la carpeta ([id], no :id).
ESLint: «Avoid constructing JSX within try/catch»
Has devuelto JSX desde dentro de un try o un catch. Saca el JSX fuera: haz que el try/catch devuelva datos (o un valor que indique el error) y construye el JSX después, como en el paso 7 del caso práctico.
next build termina bien, pero npm run lint da errores
Es lo esperado en Next 16: la compilación ya no ejecuta ESLint. Añade npm run lint a tu rutina (o a la integración continua) junto a npm run build.
Las clases de un CSS Module no se aplican
Es la regla "*.css" de la plantilla 16.4 (apartado 3.4). Aplica el arreglo del módulo 06.
Cambios que no aparecen o errores fantasma
Para el servidor, borra la carpeta .next y vuelve a arrancar. Si usas Windows y la ruta del proyecto es muy larga, muévelo a una ruta corta (apartado 1.5).
«(stale)» junto a la versión en el overlay
Hay un parche más reciente de tu versión. No es un error, pero actualiza: los parches de Next suelen incluir correcciones de seguridad. npm install next@16.4 eslint-config-next@16.4.
13. Ejercicios
Ejercicio 1: Tu entorno, por escrito
Comprueba tu versión de Node y de npm, crea un proyecto prueba-entorno con create-next-app y los valores por defecto, y averigua tres datos: la versión de next instalada, la versión de React que declara package.json y la versión de React que usa de verdad el App Router.
Ver solución
node -v
npm -v
npx create-next-app@latest prueba-entorno --yes
cd prueba-entorno
npx next --version
npx next info
npx next --version responde Next.js v16.4.3 (o el parche que se haya instalado). La versión de React de package.json aparece también en next info (react: 19.3.0). La que usa el App Router está en la copia interna de Next, y se puede preguntar a la propia copia:
node -p "require('next/dist/compiled/react').version"
19.3.0-canary-278794d7-20261002
Ojo: el package.json de esa copia no sirve, porque se llama react-builtin y no tiene campo version. La diferencia entre las dos versiones se explica en el apartado 2.5: el App Router usa siempre la versión canary que trae Next.
Ejercicio 2: Puertos sin choques
Arranca la API EMPRESA y, sin modificar ningún fichero, arranca tu proyecto de Next en el puerto 3001. Después haz el cambio permanente para dev y para start.
Ver solución
Sin tocar ficheros, cualquiera de estas dos órdenes:
npm run dev -- -p 3001
PORT=3001 npm run dev
En PowerShell, la segunda sería $env:PORT = "3001"; npm run dev.
Para hacerlo permanente, en package.json:
{
"scripts": {
"dev": "next dev -p 3001",
"start": "next start -p 3001"
}
}
Sin los dos guiones de la primera orden, npm interpretaría -p como una opción suya y Next arrancaría en el 3000, donde ya está la API.
Ejercicio 3: ¿Qué va a Git?
Clasifica estos ficheros en «se sube a Git» o «no se sube», y justifica cada uno: package-lock.json, next-env.d.ts, .next/, AGENTS.md, .env.local, tsconfig.json, node_modules/, eslint.config.mjs.
Ver solución
| Fichero | ¿A Git? | Motivo |
|---|---|---|
package-lock.json | Sí | Garantiza que todo el equipo instala las mismas versiones exactas |
next-env.d.ts | No | Lo regenera Next y cambia entre desarrollo, producción y versiones |
.next/ | No | Salida de compilación y caché; se regenera |
AGENTS.md | Sí | Si no lo subes, next dev lo vuelve a crear y aparece como cambio pendiente |
.env.local | No | Puede contener secretos y es propio de cada equipo |
tsconfig.json | Sí | Configuración del proyecto |
node_modules/ | No | Se reconstruye con npm install a partir del lockfile |
eslint.config.mjs | Sí | Configuración del proyecto |
El .gitignore de la plantilla ya ignora los cuatro que no se suben.
Ejercicio 4: Rutas tipadas
En tu proyecto, activa typedRoutes, crea la página /departamentos y añade en la portada dos enlaces: uno correcto a /departamentos y otro con una errata, /departamento. Comprueba qué dice TypeScript y qué hace next build.
Ver solución
next.config.ts:
const nextConfig: NextConfig = {
// ...lo que ya tenías
typedRoutes: true,
};
app/departamentos/page.tsx:
export default function Departamentos() {
return <h1>Departamentos</h1>;
}
En app/page.tsx:
<Link href="/departamentos">Departamentos</Link>
<Link href="/departamento">Con errata</Link>
Tras regenerar los tipos (npx next typegen, o guardando con next dev en marcha), npx tsc --noEmit da un error TS2820 en la línea del segundo enlace, del tipo:
error TS2820: Type '"/departamento"' is not assignable to type 'UrlObject | RouteImpl<"/departamento">'. Did you mean '"/departamentos"'?
next build se detiene en la fase «Running TypeScript» con el mismo error y «Failed to type check». En next dev, en cambio, la página se ve y el enlace erróneo lleva a la página 404: el modo desarrollo no comprueba tipos.
Ejercicio 5: Arreglar la ficha de empleado
Partiendo de esta página, que hace fallar next build con cacheComponents, modifícala para que compile y explica qué símbolo tendrá la ruta en la tabla:
export default async function PaginaEmpleado(props: PageProps<"/empleados/[id]">) {
const { id } = await props.params;
return <h1>Empleado {id}</h1>;
}
Ver solución
import { Suspense } from "react";
async function Detalle({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
return <h1>Empleado {id}</h1>;
}
export default function PaginaEmpleado(props: PageProps<"/empleados/[id]">) {
return (
<Suspense fallback={<p>Cargando empleado...</p>}>
<Detalle params={props.params} />
</Suspense>
);
}
La clave es que la página ya no espera params: pasa la promesa a un hijo, y el hijo la espera dentro de <Suspense>. La ruta aparece como ◐ /empleados/[id] (Partial Prerender): el fallback se genera al compilar y el contenido real llega en cada petición.
Envolver en <Suspense> el <h1> de la página original no sirve: el await está en el propio componente de la página, que queda fuera de cualquier <Suspense> que él mismo devuelva.
Ejercicio 6: El primer aviso de ESLint
Añade a la portada <img src="/next.svg" alt="Logo de Next" />, ejecuta npm run lint y corrige el aviso con el componente que sugiere.
Ver solución
ESLint avisa con la regla @next/next/no-img-element:
warning Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` ... @next/next/no-img-element
Corrección:
import Image from "next/image";
// ...
<Image src="/next.svg" alt="Logo de Next" width={100} height={20} />
next/image necesita width y height (o fill) para reservar el espacio antes de que cargue la imagen y evitar saltos en la página. Lo verás en el módulo 07. Es un warning, no un error: npm run lint termina con código 0, pero en un equipo serio conviene dejar el proyecto sin avisos.
Ejercicio 7: Comprobación de tipos para integración continua
Añade un script typecheck que compruebe los tipos de todo el proyecto, incluidas las rutas, sin compilar la aplicación. Demuestra que funciona provocando un error.
Ver solución
{
"scripts": {
"typecheck": "next typegen && tsc --noEmit"
}
}
next typegen genera .next/types y next-env.d.ts; sin ese paso, en un repositorio recién clonado tsc no encontraría PageProps ni LayoutProps y daría errores falsos. Para provocar un error, escribe PageProps<"/empleado/[id]"> (sin «s») en cualquier página:
error TS2344: Type '"/empleado/[id]"' does not satisfy the constraint 'AppRoutes'.
npm run typecheck termina con código distinto de cero, que es lo que necesita un servidor de integración continua para marcar el paso como fallido.
Ejercicio 8: Busca en la documentación local
Sin abrir el navegador, encuentra en la documentación instalada con tu Next: (a) qué valores admite logging.browserToTerminal, (b) en qué carpeta escribe next typegen sus ficheros en desarrollo y en producción.
Ver solución
La documentación está en node_modules/next/dist/docs/. Puedes buscar con el buscador de tu editor o desde el terminal:
grep -rl "browserToTerminal" node_modules/next/dist/docs/01-app/03-api-reference/05-config
grep -n "Output files" node_modules/next/dist/docs/01-app/03-api-reference/06-cli/next.md
(a) En 05-config/01-next-config-js/logging.md: 'warn' (por defecto, avisos y errores), 'error', true (todo) y false (nada).
(b) En 06-cli/next.md: los ficheros se escriben en <distDir>/types, que normalmente es .next/dev/types en desarrollo y .next/types en producción.
Ejercicio 9: Habla con el servidor MCP
Con next dev en marcha, usa curl para pedir al servidor MCP la lista de herramientas y después llama a get_project_metadata. ¿Qué dos datos devuelve?
Ver solución
curl -s -X POST http://localhost:3001/_next/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
curl -s -X POST http://localhost:3001/_next/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_project_metadata","arguments":{}}}'
La segunda respuesta, en el proyecto del módulo, fue:
event: message
data: {"result":{"content":[{"type":"text","text":"{\"projectPath\":\"C:\\\\proyectos\\\\empresa\",\"devServerUrl\":\"http://127.0.0.1:5501\"}"}]},"jsonrpc":"2.0","id":3}
Devuelve la ruta del proyecto (projectPath) y la URL del servidor de desarrollo (devServerUrl). La respuesta llega en formato Server-Sent Events (event: y data:), por eso necesitas la cabecera Accept con text/event-stream. En PowerShell, usa curl.exe en lugar de curl y escapa las comillas del JSON, o guarda el cuerpo en un fichero y pásalo con -d @peticion.json.
Ejercicio 10: Monta EMPRESA
Sigue el caso práctico completo del apartado 11 y añade una cosa más: una página /departamentos que use la misma técnica que /estado-api para mostrar el número total de departamentos consultando /api/departamentos (los listados de la API devuelven { contenido, pagina, tamano, total, paginas }).
Ver solución
// app/departamentos/page.tsx
import { Suspense } from "react";
const API_URL = process.env.API_URL ?? "http://localhost:3000/api";
type Pagina = { total: number };
async function contarDepartamentos(): Promise<number | null> {
try {
const respuesta = await fetch(`${API_URL}/departamentos`);
if (!respuesta.ok) return null;
const datos: Pagina = await respuesta.json();
return datos.total;
} catch {
return null;
}
}
async function TotalDepartamentos() {
const total = await contarDepartamentos();
if (total === null) return <p>No se puede obtener el dato de la API.</p>;
return <p>Hay {total} departamentos.</p>;
}
export default function Departamentos() {
return (
<main className="p-12">
<h1 className="text-2xl font-semibold">Departamentos</h1>
<Suspense fallback={<p>Consultando...</p>}>
<TotalDepartamentos />
</Suspense>
</main>
);
}
Con la API de ejemplo la página muestra «Hay 7 departamentos.». En la tabla de next build la ruta aparece como ◐ /departamentos. Como en la página de estado, el try/catch devuelve un dato (number | null) y el JSX se construye fuera, para cumplir la regla react-hooks/error-boundaries. Solo se usa total, así que el tipo Pagina declara solo ese campo: TypeScript no exige describir todo lo que devuelve la API, solo lo que vas a leer.
Ejercicio 11 (propuesto): El mismo proyecto en Next.js 15
Crea un proyecto con npx create-next-app@15 empresa15 --yes --eslint, copia en él las páginas del caso práctico y compílalo con npm run build. Anota qué cambia en la tabla de rutas, qué mensajes nuevos aparecen y qué tienes que quitar de next.config.ts para que arranque.
Ejercicio 12 (propuesto): Un AGENTS.md para tu equipo
Añade a AGENTS.md, fuera del bloque gestionado por Next, una sección con las convenciones de tu proyecto (idioma, puertos, comandos de comprobación). Arranca next dev, para el servidor y comprueba con git diff que Next ha respetado tu texto.
Resumen
| Concepto | API | Idea clave |
|---|---|---|
| Requisitos | Node 20.9+, TypeScript 5.1+ | Next 16 sube los mínimos; Next 15 admitía Node 18.18 |
| Crear proyecto | npx create-next-app@latest nombre --yes | --yes usa preferencias guardadas o valores por defecto; --reset-preferences las borra |
| Plantilla 16 | App Router, TypeScript, Tailwind, ESLint, AGENTS.md | next.config.ts trae cacheComponents y partialPrefetching |
| Plantilla 15 | --turbopack en scripts, Tailwind por PostCSS | Con --yes no instala ESLint |
| React del App Router | next/dist/compiled/react | Next usa su propia copia canary, no la de package.json |
| Puerto | -p 3001, PORT, scripts | Next y la API EMPRESA usan el 3000 por defecto |
| Configuración | next.config.ts con NextConfig | Tipada; un cambio reinicia next dev |
| CSS Modules en la plantilla | turbopack.rules["*.css"] | La regla de Tailwind los rompe: arreglo en el módulo 06 |
| Turbopack | next dev, next build, --webpack | Predeterminado desde 16.0; caché en disco desde 16.1 |
| Raíz del proyecto | turbopack.root | Los lockfiles de carpetas superiores generan avisos |
| Desarrollo | next dev | Compila por ruta, Fast Refresh conserva el estado del cliente |
| Errores en desarrollo | Overlay e indicador | Código fuente original; «stale» si hay parche nuevo |
| Logs del navegador | logging.browserToTerminal | [browser] en el terminal, con fichero y línea (16.2) |
| Carpeta de desarrollo | .next/dev, lock | dev y build a la vez; aviso si ya hay un next dev |
| Producción | next build, next start | Comprueba tipos y prerenderiza; ○ estático, ◐ parcial, ƒ dinámico |
| Cache Components | <Suspense> | Datos dinámicos fuera de <Suspense> detienen el build |
| Tipos de rutas | next typegen, PageProps, LayoutProps | Globales, generados de app/; desde 15.5 |
| Rutas tipadas | typedRoutes: true | Un href inexistente es un error de TypeScript |
next-env.d.ts | Generado | No se edita ni se sube a Git |
| ESLint | eslint.config.mjs, npm run lint | Flat config; next lint eliminado y build sin linter en 16 |
JSX y try/catch | react-hooks/error-boundaries | El try/catch devuelve datos; el JSX se construye fuera |
| Documentación local | node_modules/next/dist/docs/ | La de tu versión exacta; la leen los agentes y tú |
AGENTS.md | agentRules | Bloque gestionado por next dev; tu texto se respeta |
| MCP | /_next/mcp, next-devtools-mcp | Rutas, errores y compilación para asistentes (16.0) |
| Diagnóstico | next info | Versiones del sistema y aviso de actualización |