Este README explica cómo crear una aplicación Next.js desde cero, los comandos clave y una introducción práctica a su estructura y conceptos (enrutado, componentes, datos, estilos, API, etc.). Está pensado para que cualquiera pueda arrancar y entender lo básico de un proyecto Next.js moderno (App Router).
Requisitos recomendados: Node.js ≥ 18.18 (o 20+), PNPM ≥ 9 (o NPM/Yarn). Este README usa pnpm por defecto e incluye variantes con npm cuando es útil.
- Instalación de Node.js y gestor de paquetes
- Crear un nuevo proyecto Next.js
- Pasos iniciales después de crear el proyecto
- Instalación de librerías recomendadas
- Estructura básica del proyecto
- Scripts habituales
- App Router: rutas, layouts y páginas
- Componentes: Server vs Client
- Data Fetching y caché
- API Routes (Route Handlers)
- Estilos: CSS Modules, Tailwind, etc.
- Assets, fuentes e imágenes
- Variables de entorno
- Configuración:
next.config.tsytsconfig.json - Linter, formateo y calidad
- Git, .gitignore y primer commit
- Ejecutar en local y compilar para producción
- Despliegue (Vercel y otras opciones)
- FAQ breve
Asegúrate de tener Node.js instalado.
# Ver versiones instaladas
node -v
npm -v
# (Opcional) Instalar pnpm de forma global
npm install -g pnpm
# Ver versión de pnpm
pnpm -v¿Por qué PNPM? Usa un almacén global eficiente y enlaces duros, ahorrando espacio y acelerando instalaciones. Puedes usar npm si prefieres simplicidad o no quieres instalar nada adicional.
Usaremos el generador oficial create-next-app.
# Con pnpm (recomendado)
pnpm create next-app@latest my-app
# Alternativa con npm
npm create next-app@latest my-app
# o
npx create-next-app@latest my-appDurante el asistente podrás elegir:
- TypeScript (sí, recomendado)
- ESLint (sí)
- Tailwind CSS (opcional pero recomendado)
- App Router (sí — es el router moderno de Next.js)
- /src directory (opcional; mantener la raíz simple también es válido)
- Import alias (por ejemplo
@/*)
Después de ejecutar create-next-app, sigue estos pasos antes de pasar a la estructura del proyecto. Aquí añadimos además paquetes recomendados y la configuración mínima de calidad para que el proyecto quede listo desde el primer día.
# 3.1 Entra al directorio del proyecto
cd my-app
# 3.2 Instala dependencias (si el asistente no lo hizo)
pnpm install # o npm install
# 3.3 Arranca en desarrollo para verificar
pnpm dev # o npm run dev
# abre http://localhost:3000Por qué: Estos paquetes cubren casos comunes (internacionalización, UI, toasts, formularios/validación, emails) y utilidades frecuentes.
pnpm add next-intl@^3 lucide-react sonner react-hook-form zod resend clsxnext-intl@^3: i18n para App Router (routing por locales, traducciones, formateo).lucide-react: set de iconos moderno.sonner: notificaciones/toasts minimalistas.react-hook-form+zod: formularios con validación tipada.resend: envío de emails (ideal con Route Handlers + Resend).clsx: composición de clases CSS sencilla.
Por qué: garantizar calidad (tests), estilo consistente (Prettier) y ejecutar checks automáticos en cada commit (Husky + lint-staged).
pnpm add -D vitest @testing-library/react @testing-library/jest-dom \
@types/testing-library__jest-dom \
husky lint-staged prettier prettier-plugin-tailwindcssNota:
@vitejs/plugin-reactno es necesario para Next.js. Se usa con proyectos Vite. Si vas a probar componentes en un sandbox Vite, entonces sí tendría sentido añadirlo.
Inicializa Husky (con pnpm):
pnpm dlx husky-init && pnpm exec husky install
# crea .husky/pre-commit que ejecuta "npm test" por defecto; lo adaptamos abajoSi prefieres lo que usaste antes:
npx husky inittambién funciona, pero con pnpm se recomiendapnpm dlx husky-init.
Configura lint-staged y scripts en package.json (añade o ajusta):
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "vitest run",
"test:ui": "vitest"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": ["next lint --fix --file", "prettier --write"],
"*.{md,css,json}": ["prettier --write"]
}
}Ajusta el hook pre-commit de Husky (.husky/pre-commit):
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
pnpm exec lint-staged
pnpm run test --runCrea .prettierrc si no existe:
printf '{
"singleQuote": true,
"semi": true,
"plugins": ["prettier-plugin-tailwindcss"]
}
' > .prettierrcConfig mín. para Vitest (vitest.config.ts):
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts']
}
});Archivo de setup (vitest.setup.ts):
import '@testing-library/jest-dom';Ejemplo de test (components/Counter.test.tsx):
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
test('increments counter', () => {
render(<Counter />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(button).toHaveTextContent('Clicks: 1');
});Por qué: Librería de componentes sobre Tailwind, centrada en accesibilidad y composabilidad.
Requiere Tailwind. Si lo marcaste en
create-next-app, ya está listo. Si no, añade Tailwind y configúralo antes.
Inicializa shadcn/ui:
pnpm dlx shadcn-ui@latest init -yAñade un componente de ejemplo (Button):
pnpm dlx shadcn-ui@latest add buttonUso rápido:
import { Button } from "@/components/ui/button";
export default function Demo() {
return <Button>Click me</Button>;
}next-intl (App Router) — estructura mínima
// app/[locale]/layout.tsx
import { NextIntlClientProvider, useMessages } from 'next-intl';
import type { ReactNode } from 'react';
export default function LocaleLayout({ children, params }: { children: ReactNode; params: { locale: string } }) {
const messages = useMessages();
return (
<html lang={params.locale}>
<body>
<NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>
</body>
</html>
);
}// app/[locale]/page.tsx
import { getTranslations } from 'next-intl/server';
export default async function Home() {
const t = await getTranslations('Home');
return <h1>{t('title')}</h1>; // Home.title en tus mensajes
}Mensajes
# messages/es.json
{ "Home": { "title": "Hola Next.js" } }
# messages/en.json
{ "Home": { "title": "Hello Next.js" } }
Formulario con RHF + Zod
'use client';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
const schema = z.object({ email: z.string().email(), name: z.string().min(2) });
type FormData = z.infer<typeof schema>;
export default function SimpleForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({ resolver: zodResolver(schema) });
const onSubmit = (data: FormData) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input placeholder="Email" {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
<input placeholder="Name" {...register('name')} />
{errors.name && <p>{errors.name.message}</p>}
<button type="submit">Send</button>
</form>
);
}Toasts con sonner
'use client';
import { Toaster, toast } from 'sonner';
export default function ToastDemo() {
return (
<div>
<button onClick={() => toast.success('Saved!')}>Show toast</button>
<Toaster richColors />
</div>
);
}Email con Resend
// app/api/send-email/route.ts
import { Resend } from 'resend';
import { NextResponse } from 'next/server';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(request: Request) {
const { to, subject, html } = await request.json();
const result = await resend.emails.send({ from: 'MyApp <onboarding@myapp.dev>', to, subject, html });
return NextResponse.json(result);
}Con esto, el proyecto queda listo para trabajar (i18n, UI, formularios, toasts, emails, tests, formateo y hooks de calidad).
Para proyectos reales conviene añadir librerías de internacionalización, formularios, validación, notificaciones, testing y tooling. Aquí una lista base:
# Utilidades principales
pnpm add next-intl@^3 lucide-react sonner react-hook-form zod resend clsx- next-intl: internacionalización (traducciones)
- lucide-react: iconos SVG
- sonner: notificaciones
- react-hook-form: gestión de formularios
- zod: validación de datos
- resend: envío de emails
- clsx: concatenar clases de forma segura
# Dependencias de desarrollo (testing, formateo y calidad)
pnpm add -D vitest @testing-library/react @testing-library/jest-dom \
@types/testing-library__jest-dom @vitejs/plugin-react \
husky lint-staged prettier prettier-plugin-tailwindcss- vitest: framework de tests rápido
- Testing Library: utilidades para testear componentes
- Husky + lint-staged: ejecutar checks en pre-commit
- Prettier + plugin tailwind: formateo consistente
# UI Components con shadcn
pnpm dlx shadcn-ui@latest init -y- shadcn-ui: sistema de componentes accesibles + tailwind
# Inicializar husky (hooks de git)
npx husky initCon esto el proyecto queda listo para desarrollo real: buenas prácticas de DX, testing, estilos consistentes y componentes reutilizables.
Con App Router, la estructura típica es:
my-app/
├─ app/
│ ├─ layout.tsx # Layout raíz (envoltorio de todas las páginas)
│ ├─ page.tsx # Página para ruta "/"
│ ├─ about/
│ │ └─ page.tsx # Página para ruta "/about"
│ ├─ api/ # Rutas de API (route handlers)
│ │ └─ hello/route.ts # Endpoint en "/api/hello"
│ └─ (marketing)/ # Segmentos con nombre (agrupación lógica)
├─ public/ # Archivos estáticos (imágenes, favicon, etc.)
├─ styles/ # Estilos globales o base (si se generan)
├─ components/ # Componentes reutilizables
├─ lib/ # Utilidades, servicios, clientes http, etc.
├─ next.config.ts # Configuración de Next.js
├─ package.json
├─ pnpm-lock.yaml # o package-lock.json / yarn.lock
└─ tsconfig.json # Config de TypeScript
¿Qué es app/? Define rutas por convención de carpetas y fomenta componentes de servidor por defecto. Cada carpeta puede tener:
page.tsx: Página renderizable en esa ruta.layout.tsx: Layout que envuelve a laspage.tsxhijas.loading.tsx: UI de carga para la ruta.error.tsx: UI de error para la ruta.route.ts: Endpoint HTTP (si está dentro deapp/api/...).template.tsx: Similar a layout, pero se recrea en cada navegación.
En package.json encontrarás scripts como:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}Comandos:
# desarrollo con HMR
dpnm dev # (o) pnpm dev
npm run dev
# compilar para producción
pnpm build
npm run build
# arrancar en modo producción (tras build)
pnpm start
npm run start
# ejecutar linter
pnpm lintNota: para ver la app en local, abre
http://localhost:3000.
// app/page.tsx
export default function HomePage() {
return (
<main>
<h1>Hello Next.js</h1>
<p>Esta es la home (ruta "/").</p>
</main>
);
}Define HTML y UI compartida.
// app/layout.tsx
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'My App',
description: 'Demo Next.js',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es">
<body>
<header>My App</header>
{children}
<footer>© {new Date().getFullYear()}</footer>
</body>
</html>
);
}mkdir app/about && printf "export default function About(){return <h2>About</h2>}" > app/about/page.tsx// components/Nav.tsx
import Link from 'next/link';
export default function Nav() {
return (
<nav>
<Link href="/">Home</Link> | <Link href="/about">About</Link>
</nav>
);
}Incluye Nav en tu layout o páginas para que aparezca en todas.
- Server Components (por defecto): se renderizan en el servidor, mejor rendimiento, pueden acceder a secretos y hacer fetch directamente.
- Client Components: necesarios para interactividad (estado, efectos, eventos). Se declaran con la directiva
'use client'al inicio del archivo.
// components/Counter.tsx
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((c) => c + 1)}>Clicks: {count}</button>
);
}Usa componentes de servidor cuando no necesites estado/eventos y componentes cliente para UI interactiva.
En App Router, puedes hacer fetch directamente en componentes server. Next gestiona la caché, revalidación y renderizado.
// app/users/page.tsx
async function getUsers() {
const res = await fetch('https://jsonplaceholder.typicode.com/users', {
// revalidar cada 60s (ISR)
next: { revalidate: 60 },
});
if (!res.ok) throw new Error('Error al cargar usuarios');
return res.json() as Promise<Array<{ id: number; name: string }>>;
}
export default async function UsersPage() {
const users = await getUsers();
return (
<main>
<h1>Usuarios</h1>
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
</main>
);
}Modos de fetch y caché (resumen):
- Estático (por defecto): la respuesta se cachea en build.
- ISR: usa
next: { revalidate: N }para regenerar cada N segundos. - Dinámico: añade
cache: 'no-store'o leecookies()/headers()para desactivar caché.
await fetch(url, { cache: 'no-store' }); // Fuerza dinámicoPuedes crear endpoints dentro de app/api/**/route.ts.
// app/api/hello/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ message: 'Hello from API' });
}
export async function POST(request: Request) {
const body = await request.json();
return NextResponse.json({ ok: true, received: body }, { status: 201 });
}- Soporta métodos
GET,POST,PUT,PATCH,DELETE. - Se ejecuta en Edge o Node según configuración (por defecto Node). Puedes exportar
runtime = 'edge'.
Opciones comunes:
- CSS Modules: scoping por archivo (
.module.css). - Tailwind CSS: utilidades con clases; rápido para prototipado.
- Styled JSX/Styled Components/Vanilla Extract: otras alternativas.
// components/Box.tsx
import styles from './Box.module.css';
export default function Box() {
return <div className={styles.box}>Caja</div>;
}/* components/Box.module.css */
.box { padding: 1rem; border: 1px solid #ddd; border-radius: 8px; }export default function Card({ title }: { title: string }) {
return (
<div className="rounded-xl border p-4 shadow-sm">
<h3 className="text-lg font-semibold">{title}</h3>
</div>
);
}- Coloca estáticos en
public/y referencia con/archivo.png. - Usa
next/imagepara imágenes optimizadas. - Usa
next/fontpara cargar fuentes locales o de Google.
// Ejemplo next/image
import Image from 'next/image';
export default function Avatar() {
return <Image src="/avatar.png" alt="Avatar" width={64} height={64} />;
}// Ejemplo next/font (Google)
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function Text() {
return <p className={inter.className}>Texto con Inter</p>;
}Crea archivos .env y .env.local (no subas secretos a git). Para exponer variables al cliente deben empezar por NEXT_PUBLIC_.
# .env.local (ejemplo)
DATABASE_URL=postgres://...
NEXT_PUBLIC_API_BASE=/apiLee variables en server components o route handlers con process.env.X.
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactStrictMode: true,
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'images.example.com' }
]
}
};
export default nextConfig;{
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["./*"] },
"strict": true,
"jsx": "preserve",
"esModuleInterop": true,
"moduleResolution": "bundler"
}
}- ESLint: reglas de calidad (incluye plugin de Next por defecto).
- Prettier: formateo consistente.
# Instalar Prettier (si no venía)
pnpm add -D prettier
# .prettierrc (ejemplo)
printf '{\n "singleQuote": true,\n "semi": true\n}\n' > .prettierrc
# Ejecutar
pnpm lintConsejo: configura hooks con Husky + lint-staged para validar antes de cada commit.
git init
# .gitignore recomendado (si no lo generó el asistente)
printf "node_modules\n.next\n.env*\n.vercel\n" > .gitignore
git add .
git commit -m "chore: bootstrap Next.js app"
git branch -M main
# Crea el repo en GitHub y vincula
# git remote add origin git@github.com:usuario/my-app.git
# git push -u origin main# desarrollo
pnpm dev
# build producción
pnpm build
# ejecutar build
pnpm start- Dev por defecto:
http://localhost:3000. next buildhace análisis, tree-shaking y prepara artefactos optimizados.
Vercel (opción más simple y nativa):
# Instalar CLI (opcional)
pnpm add -g vercel
# Deploy interactivo desde la carpeta del proyecto
vercel
# Para produccion
vercel --prodOtras opciones: contenedor Docker + Node, plataformas como Netlify, Render, Fly.io, Railway, etc. En Node, ejecuta next build y sirve con next start detrás de un reverse proxy.
¿App Router o Pages Router? Usa App Router (carpeta app/) para proyectos nuevos. pages/ sigue soportado pero es el enfoque legado.
¿TypeScript obligatorio? No, pero es muy recomendado por DX y robustez.
¿PNPM vs NPM? PNPM suele ser más rápido y ahorra espacio. NPM viene con Node y es suficiente si prefieres simplicidad.
¿Cómo organizo componentes y lógica? Crea carpetas components/ y lib/ para separar UI de utilidades/servicios. Reutiliza layouts y usa Server Components por defecto.
# Crear proyecto
pnpm create next-app@latest my-app
cd my-app
pnpm install
# Desarrollo
pnpm dev
# Linter
pnpm lint
# Build + start prod
pnpm build
pnpm start
# (Opcional) Tailwind se incluye si lo seleccionaste en el asistente
# Git básico
git init && git add . && git commit -m "chore: bootstrap"// app/users/loading.tsx
export default function Loading() {
return <p>Cargando usuarios…</p>;
}
// app/users/error.tsx
'use client';
export default function Error({ error }: { error: Error }) {
return <p>Ha ocurrido un error: {error.message}</p>;
}// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>Post: {params.slug}</h1>;
}// app/search/page.tsx
export default function SearchPage({ searchParams }: { searchParams: { q?: string } }) {
return <h1>Buscando: {searchParams.q ?? '—'}</h1>;
}