Inicio de proyecto en Next.js
La base es más barata de sentar el primer día: después, cada regla hay que implementarla sobre código que ya la viola. Si existe la plantilla frontend-starter — empieza con una copia de la plantilla: en ella los pasos siguientes ya están hechos y probados, y este documento queda como explicación de las decisiones y lista de verificación de auditoría.
Este documento define el orden de pasos y el conjunto mínimo. Cómo escribir el código en sí — en el skill fsd-frontend (si existe en el proyecto), la autorización — en frontend-auth-session, la revisión previa al commit — en anti-slop-review.
Las configuraciones listas — en references/configs.md, la estructura Next + FSD con ejemplos de código — en references/next-structure.md.
Prioridad de reglas. Las instrucciones explícitas del usuario y los archivos agent del proyecto (AGENTS.md, CLAUDE.md) tienen prioridad sobre este documento. Las versiones de las librerías cambian: antes de copiar una configuración, verifícala con la documentación de la versión instalada y no afirmes que funciona hasta que la hayas ejecutado.
Hechos sobre Next.js — de la documentación en el paquete, no de este documento ni de la memoria. Desde la versión 16.2, la documentación está en node_modules/next/dist/docs/, y el AGENTS.md del proyecto la referencia. Ante cualquier discrepancia con los ejemplos de aquí, la documentación de la versión instalada es la que manda.
Notaciones: MUST — no se puede violar; SHOULD — por defecto sí, la desviación requiere una razón.
1. Decisiones antes del código
Cada decisión de la tabla se registra en el README (sección «Decisiones») o en un ADR, con su razón. No se toma una desviación del valor por defecto sin una razón registrada.
| Pregunta | Por defecto | Cuándo hacerlo diferente |
|---|---|---|
| Next.js o SPA | Next.js, si se necesita al menos uno de: SSR/SEO, BFF (secretos, HttpOnly-cookie), route handlers | Aplicación interna tras autenticación sin SEO ni BFF → Vite + React Router: compilación y despliegue más simples |
| Router | App Router | Pages Router — solo en un proyecto existente |
| Monorepo | No, una sola aplicación | Cuando realmente aparezca una segunda aplicación o paquete → pnpm workspaces; Nx/Turborepo — cuando la compilación o el CI empiecen a ralentizarse |
| Multi-Zones, microfrontends | No | Equipos independientes con lanzamientos independientes |
| Datos del servidor | Server Components para la carga inicial; TanStack Query para datos interactivos del cliente y mutaciones | Aplicación puramente cliente → solo TanStack Query |
| Textos e i18n | Módulo de traducciones desde el primer día (next-intl o similar), incluso para un solo idioma, si el proyecto sigue fsd-frontend | Decisión explícita «sin i18n» registrada en el README |
| Gestor de paquetes | pnpm, versión en packageManager | — |
| Despliegue | Decidir de inmediato: Vercel, servidor Node (output: "standalone") o Docker | De esto dependen env, caché, CI |
MUST: no agregar tecnología «para el futuro». Monorepo, segundo router, Storybook, gestor de estado, MSW «como tipo de backend» aparecen cuando tienen su primer consumidor real.
2. Orden de pasos
- Crear proyecto (
pnpm create next-app@latest): TypeScript, ESLint, Tailwind, App Router. FijarpackageManager,engines.node(Next 16 requiere Node 20.9+) y.nvmrc. - Eliminar inmediatamente la plantilla del generador: página de inicio, SVG de
public/, README de plantilla, estilos demo. Reemplazar el README con el esqueleto dereferences/configs.md. Una plantilla dejada «para después» llega a producción.AGENTS.mdyCLAUDE.mddecreate-next-appno se eliminan: el bloque entre<!-- BEGIN:nextjs-agent-rules -->y<!-- END:nextjs-agent-rules -->indica a los agentes la documentación de la versión instalada,next devlo mantiene solo. Las reglas propias del proyecto se escriben fuera de estos marcadores. - TypeScript estricto (sección 4 de
references/configs.md) y scripttypecheck(tsc --noEmit). - Estructura FSD sobre Next (sección 3 a continuación).
- Linter y formateo: ESLint flat config de
eslint-config-next+ reglas type-aware + linter de límites + Prettier. Next 16 eliminónext linty no lint ennext build— el linter se ejecuta como script independiente y en CI. - Configuración de entorno (sección 4).
- Modelo de errores y cliente API en
shared/api(sección 5). - Providers en la capa
_app: QueryClient, tema, i18n, servicios globales (toast, confirm) como puerto enshared+ implementación en_app. - Base de UI (sección 6).
- Pruebas: Vitest + Testing Library + MSW, Playwright para e2e (sección 7).
- Git hooks: husky + lint-staged (eslint --fix, prettier) + commitlint.
- CI: lint, typecheck, test, build en cada push y PR (sección 8).
- Seguridad por defecto (sección 9).
- README relleno con lo que ya es cierto (sección 10), y Definition of Done completado (sección 11).
Los pasos 1–3 son un commit separado «scaffold»: así el historial muestra lo que hizo el generador y lo que hiciste tú.
3. FSD sobre el enrutamiento de Next
Según la guía oficial de FSD para App Router: la carpeta de servicio de Next app/ — en la raíz del proyecto, las capas de FSD — en src/, y las capas app y pages reciben el prefijo _ para no confundirse con las carpetas de servicio de Next.
app/ enrutamiento Next: solo archivos de rutas
├── layout.tsx layout raíz → providers de src/_app
├── (auth)/sign-in/page.tsx export { SignInPage as default } from "@/_pages/sign-in"
└── api/auth/login/route.ts route handler (BFF), delgado
proxy.ts Next 16+ (antes middleware.ts), junto a app/
src/
├── _app/ capa app: providers, estilos globales, implementaciones de servicios
├── _pages/ capa pages
├── widgets/ (si el proyecto los usa)
├── features/
├── entities/
└── shared/{ui,api,lib,config}
MUST:
- Los archivos en
app/son delgados: reexportación de página desdesrc/_pages,generateMetadata, análisis deparams/searchParams(en Next 15+ sonPromise, hay que hacerlesawait) y paso como props. No contienen lógica. - Los route handlers y Server Actions son delgados: analizar y validar entrada → llamar a una función desde
src→ devolver respuesta. Cada uno es un punto de entrada público: verifica la autorización por sí mismo, no depende deproxy.ts. - No crear
src/appnisrc/pages: Next busca las carpetas de servicio tanto en la raíz como ensrc, y una carpeta con ese nombre se convierte fácilmente en un segundo router. - El linter de límites conoce los prefijos: patrones
src/_app,src/_pages/*en lugar desrc/app,src/pages/*(config —fsd-frontend,references/conventions.md, sección 11). "use client"se coloca lo más abajo posible en el árbol. La página por defecto es del servidor, las del cliente son hojas con interactividad.- Los módulos con secretos comienzan con
import "server-only", los módulos conwindow— conimport "client-only".
Detalladamente y con código: references/next-structure.md.
4. Configuración de entorno
MUST:
- Las variables se leen en dos módulos:
shared/config/env.server.ts(import "server-only") yshared/config/env.client.ts. Esquema —zod, validación al importar: el error de configuración se produce al inicio, no en la primera solicitud. En el resto del código,process.envestá prohibido (regla de linter). NEXT_PUBLIC_*se incrustan en el bundle al compilar y solo con acceso directoprocess.env.NEXT_PUBLIC_X: sin desestructuración niprocess.env[name]. Una misma imagen para varios entornos con diferentes valores públicos no se puede compilar así — esos valores se leen en el servidor y se pasan hacia abajo.- Los secretos — solo sin el prefijo
NEXT_PUBLIC_. .env.examplecontiene cada variable con un comentario y un valor seguro;.env*en.gitignore, excepto.env.example. La compilación en CI se realiza con los valores de.env.example.
5. Modelo de errores y cliente API
MUST:
- Los errores se normalizan en
AppErrorconkinduna sola vez, en el límite deshared/api(modelo —fsd-frontend,references/conventions.md, sección 1). Por encima deshared/api, nadie analizaResponse, códigos de estado ni textos de error. - Un solo
request()sobrefetch(o un cliente generado, envuelto de la misma manera): propagasignal, convierte el error de red ennetwork, el código de estado HTTP enkind, valida el cuerpo de la respuesta con un esquema. Ejemplo —references/next-structure.md. - Tipo de error según el código: estado,
extensions.code(GraphQL responde con 200 también en errores),codeen el cuerpo. Si el backend solo devuelve texto — mapeo por texto en un solo mapper, con comentario de razón y pruebas. - La respuesta del backend se mapea a un tipo de dominio en el límite; los DTO no llegan a la UI.
- Los datos del usuario no se cachean entre usuarios: nada de
"use cache"/unstable_cachepara respuestas que dependen de la sesión. - El cliente se genera a partir de un esquema (OpenAPI, GraphQL), si existe. El esquema o el código generado están en el repositorio: el CI no debe consultar al backend en vivo.
SHOULD: interfaz sobre el transporte (AuthApi + fábrica) — solo cuando ya tiene un segundo consumidor (fsd-frontend, references/state-and-data.md, 2.1). No se crea un valor de configuración como type: "mock" que lance «not implemented».
6. Base de UI
- Tokens de diseño — variables CSS en
:root(y para el tema oscuro), conectadas a Tailwind v4 mediante@theme. En los componentes — solo tokens; valores HEX crudos y valores arbitrarios (text-[#3a3a3a]) — excepción puntual con comentario. - Las clases existen. Los componentes copiados de shadcn/ui y ejemplos vienen con sus propios tokens (
bg-card,text-muted-foreground). O introduces esos tokens o reemplazas las clases — de lo contrario, el componente se queda sin estilos en silencio. - Los primitivos de
shared/uise construyen sobre una librería headless (Radix, React Aria) por accesibilidad; variantes —cva, unión de clases —cn(clsx+tailwind-merge). - Fuentes —
next/font, imágenes —next/imagecon dimensiones,images.remotePatternslistados explícitamente. - Storybook — cuando el kit de UI es reutilizado por varias aplicaciones o equipos, no antes.
7. Pruebas
- Vitest + Testing Library + jsdom + MSW con
onUnhandledFrame: "error"(MSW 3; en MSW 2 —onUnhandledRequest). Configuración —references/configs.md. - Vitest no puede renderizar
asyncServer Components. Extrae la lógica de ellos a funciones y pruébala con pruebas unitarias, y la página misma — en e2e. - Playwright para 3–10 caminos críticos. En CI, e2e se ejecutan contra un backend predecible (servidor mock, entorno con base de datos de pruebas), no contra un entorno compartido donde las pruebas modifican datos de otros.
- La primera prueba se escribe el día cero: verifica que la infraestructura de pruebas funciona y establece un ejemplo.
8. CI
MUST: en cada push a la rama principal y en cada PR: lint, typecheck, test, build. Plantilla de GitHub Actions — references/configs.md. Para repositorios públicos, Actions es gratuito; para privados, hay una cuota mensual de minutos.
- La compilación en CI se realiza con
.env.example, sin secretos. - e2e — como job independiente, cuando hay un backend aislado; hasta entonces, una nota explícita en el README explicando por qué no están en CI.
- La insignia de CI en el README aparece junto con el workflow.
9. Seguridad por defecto
poweredByHeader: false; cabeceras medianteheaders()ennext.config.ts:X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin,X-Frame-Options: DENY(oframe-ancestorsen CSP). CSP con nonce — una tarea aparte, pero la decisión «está / no está y por qué» se registra de inmediato.- Server Actions y route handlers son endpoints públicos: validación de entrada con esquema y verificación de permisos en cada uno.
dangerouslySetInnerHTML— solo con sanitizador y comentario de razón.- Los datos personales y tokens no se registran en logs.
- Autorización — según
frontend-auth-session.
10. README
Esqueleto — references/configs.md. La regla es una: en el README solo lo que es cierto para el código en este commit. Cada sección se actualiza en el mismo commit que el código que describe. Los planes — en una sección independiente «Siguiente», con la nota de que no están implementados.
11. Definition of Done para el inicio
- La plantilla del generador se ha eliminado; el README es un esqueleto relleno con la verdad.
-
packageManager,engines.node,.nvmrcestán fijados. - TS strict;
typecheck,lint,test,buildse ejecutan localmente (se han ejecutado, no se asumen). - Estructura FSD con prefijos
_app/_pages; el linter de límites detecta importaciones hacia arriba. - Env mediante dos módulos con esquema;
process.envfuera de ellos está prohibido por el linter;.env.examplecompleto. -
AppErroryrequest()enshared/api, cubiertos con pruebas (red, estados, esquema incorrecto). - Providers en
_app; QueryClient no es compartido entre usuarios en el servidor. - Tokens de diseño en variables CSS; todas las clases utilizadas existen.
- Vitest + MSW + Playwright configurados, al menos una prueba viva de cada uno.
- Git hooks y commitlint funcionan.
- CI en verde.
- Cabeceras de seguridad y
poweredByHeader: false. - Decisiones de la sección 1 registradas.
12. Cuando tengas dudas
- Entre «agregar ahora» y «agregar cuando sea necesario» — cuando sea necesario.
- Entre «configurar manualmente» y «dejar como lo generó create-next-app» — déjalo, hasta que haya una razón para cambiarlo, y registra la razón cuando lo cambies.
- Entre «una aplicación» y «monorepo» — una aplicación.
- Entre «configuración de memoria» y «configuración de la documentación de la versión actual» — de la documentación.