Cheatsheet Next.js
Framework React full-stack com renderização no servidor e App Router
Next.js
Setup e CLI
Crear Proyecto
npx create-next-app@latest mi-app cd mi-app npm run dev
create-next-app genera un proyecto completo con App Router, TypeScript y ESLint. npm run dev inicia el servidor de desarrollo en localhost:3000 con hot reload.
TypeScript
// app/page.tsx
export default function Home() {
return <h1>Hola</h1>;
}
// Types para params:
export default function Post({
params,
}: { params: { slug: string } }) {}Next.js tiene soporte nativo de TypeScript. Archivos .tsx para componentes con JSX. El tsconfig.json se genera automáticamente. Los types para params y searchParams se infieren.
ESLint y Prettier
// .eslintrc.json
{
"extends": "next/core-web-vitals"
}
// next lint detecta:
// - hooks rules
// - image optimization
// - link issuesnext/core-web-vitals incluye reglas React + Next.js. Detecta uso incorrecto de Image, Link y hooks. npm run lint verifica el proyecto entero.
Comandos CLI
npm run dev # desarrollo npm run build # build de producción npm run start # servir el build npm run lint # verificar código
dev para desarrollo con HMR. build compila y optimiza para producción. start sirve el build. lint ejecuta el ESLint configurado.
Variables de Entorno
// .env.local DATABASE_URL=postgres://... NEXT_PUBLIC_API_URL=https://api.example.com // En el código: process.env.DATABASE_URL // servidor process.env.NEXT_PUBLIC_API_URL // cliente
.env.local guarda secretos (no va a git). Las variables con prefijo NEXT_PUBLIC_ quedan disponibles en el cliente. Sin el prefijo, solo son accesibles en el servidor.
Estructura App Router
app/ layout.tsx # layout raíz page.tsx # página inicial (/) globals.css # estilos globales public/ # archivos estáticos next.config.js # configuración
La carpeta app/ define las rutas por archivos. layout.tsx envuelve todas las páginas. public/ sirve archivos estáticos en la raíz. next.config.js personaliza el build.
Path Aliases
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
// Uso:
import { Card } from "@/components/Card";@/ es el alias por defecto para src/ (o la raíz). Configurado en tsconfig.json con paths. Evita imports relativos anchos como ../../../components.
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
domains: ["example.com"],
},
};
module.exports = nextConfig;next.config.js configura el framework. reactStrictMode activa verificaciones extra. images.domains autoriza dominios externos para el componente Image.
Directorio src
src/
app/
layout.tsx
page.tsx
components/
Card.tsx
lib/
utils.tsLa carpeta src/ es opcional — organiza el código fuente separado de las configs. Si existe, el App Router búsqueda src/app/ en vez de app/. Recomendada para proyectos grandes.
App Router e Páginas
Página Básica
// app/about/page.tsx
export default function About() {
return <h1>Sobre nosotros</h1>;
}Cada carpeta con page.tsx se convierte en una ruta. app/about/page.tsx → /about. El componente se exporta como default. Por defecto, es un Server Component.
loading.tsx
// app/blog/loading.tsx
export default function Loading() {
return <p>Cargando posts...</p>;
}loading.tsx muestra un fallback mientras la página carga. Envuelve la página en Suspense automáticamente. Aparece instantáneamente en la navegación — mejora la UX percibida.
Route Groups
app/
(marketing)/
layout.tsx
about/page.tsx
(app)/
layout.tsx
dashboard/page.tsxLos paréntesis (nombre) crean grupos de rutas sin afectar la URL. Cada grupo puede tener su propio layout. /about y /dashboard quedan en layouts diferentes sin prefijo en la URL.
Ruta Raíz
// app/page.tsx
export default function Home() {
return (
<main>
<h1>Bienvenido</h1>
</main>
);
}app/page.tsx es la página inicial (/). Es el punto de entrada de la aplicación. Hereda el layout raíz de app/layout.tsx automáticamente.
error.tsx
"use client";
// app/blog/error.tsx
export default function Error({
error,
reset,
}: { error: Error; reset: () => void }) {
return (
<div>
<p>Error: {error.message}</p>
<button onClick={reset}>Reintentar</button>
</div>
);
}error.tsx captura errores de runtime en la sección. Debe ser Client Component ("use client"). reset() intenta re-renderizar. Funciona como error boundary automático.
Parallel e Intercepting Routes
app/
@modal/
(.)photo/[id]/page.tsx
layout.tsx # recibe {modal, children}
page.tsx
// layout.tsx:
export default function Layout({
children, modal
}) { return <>{children}{modal}</> }@slot define parallel routes (múltiples páginas en el mismo layout). (.) intercepta rutas (modal sobre la página actual). Patrón para modales tipo Instagram/Twitter.
Layout Raíz
// app/layout.tsx
export default function RootLayout({
children,
}: { children: React.ReactNode }) {
return (
<html lang="es">
<body>{children}</body>
</html>
);
}El layout raíz es obligatorio y envuelve todas las páginas. Debe contener <html> y <body>. children es la página o layout hijo. Persiste entre navegaciones (sin re-mount).
not-found.tsx
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h1>404</h1>
<p>Página no encontrada</p>
</div>
);
}not-found.tsx se renderiza cuando una ruta no existe o cuando se llama notFound() manualmente. Puede existir global o por sección. Retorna status HTTP 404.
Layout Anidado
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: { children: React.ReactNode }) {
return (
<div>
<nav>Menú del blog</nav>
{children}
</div>
);
}Los layouts en subcarpetas se aplican a todas las rutas de esa sección. app/blog/layout.tsx envuelve /blog y /blog/[slug]. Los layouts se anidan jerárquicamente sin re-render en la navegación.
template.tsx
// app/template.tsx
export default function Template({
children,
}: { children: React.ReactNode }) {
return <div className="animate">{children}</div>;
}template.tsx es como layout pero se recrea en cada navegación (nuevo mount). No preserva estado. Útil para animaciones de entrada o efectos que deben reiniciarse en cada ruta.
Componentes e Layout
Metadata Estático
// app/layout.tsx o page.tsx
export const metadata = {
title: "Mi Sitio",
description: "Descripción del sitio",
openGraph: {
title: "Mi Sitio",
images: ["/og.png"],
},
};metadata define SEO y Open Graph. Se exporta como constante en Server Components. Genera tags <title>, <meta> y og: automáticamente en el <head>.
next/image
import Image from "next/image";
<Image
src="/photo.jpg"
width={800}
height={600}
alt="Descripción"
priority
/>next/image optimiza imágenes: resize, lazy load, formatos modernos (WebP/AVIF). width/height evitan layout shift. priority desactiva lazy load para imágenes above-the-fold.
global-error y Sitemap
// app/global-error.tsx ("use client")
export default function GlobalError() {
return <html><body><h1>Error</h1></body></html>;
}
// app/sitemap.ts
export default function sitemap() {
return [{ url: "https://site.com", lastModified: new Date() }];
}global-error.tsx sustituye el layout raíz en errores fatales. sitemap.ts genera /sitemap.xml dinámicamente. Ambos son archivos de convención del App Router.
Metadata Dinámico
// app/blog/[slug]/page.tsx
export async function generateMetadata({
params,
}: { params: { slug: string } }) {
const post = await getPost(params.slug);
return { title: post.title };
}generateMetadata() genera metadatos dinámicos por página. Recibe params y searchParams. Permite buscar datos y definir title, description, etc. por ruta.
next/link
import Link from "next/link";
<Link href="/about">Acerca de</Link>
<Link href="/blog" prefetch={true}>Blog</Link>
<Link href={{ pathname: "/post", query: { id: 1 } }}>
Post
</Link>next/link hace navegación client-side sin reload. Prefetch automático de páginas visibles. Acepta string u objeto con pathname y query. Sustituye <a> para rutas internas.
Componente Compartido
// components/Card.tsx
export function Card({ title, text }: {
title: string;
text: string;
}) {
return (
<div className="card">
<h3>{title}</h3>
<p>{text}</p>
</div>
);
}Los componentes en components/ son reutilizables. Sin directiva, son Server Components por defecto. Añade "use client" si necesitas interactividad (hooks, eventos).
next/script
import Script from "next/script"; <Script src="https://analytics.example.com/script.js" strategy="afterInteractive" />
next/script optimiza la carga de scripts externos. strategy: beforeInteractive, afterInteractive (default), lazyOnload. Evita bloquear la renderización.
next/font
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
});
// layout.tsx:
<body className={inter.variable}>next/font optimiza fuentes automáticamente (zero layout shift). Descarga en el build y sirve localmente. variable crea una CSS variable para usar en Tailwind o CSS custom.
Icon y Manifest
app/
icon.png # favicon automático
apple-icon.png # iOS home screen
manifest.ts # PWA manifest
// manifest.ts:
export default {
name: "Mi App",
theme_color: "#000",
};Archivos de convención: icon.png genera favicon automáticamente. manifest.ts configura la PWA. Sin necesidad de tags manuales en el <head> — Next.js inyecta todo.
Data Fetching
Fetch en el Servidor
// app/page.tsx (Server Component)
export default async function Page() {
const res = await fetch("https://api.example.com/posts");
const posts = await res.json();
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}Los Server Components pueden ser async y usar fetch directamente. Los datos se buscan en el servidor — nunca se exponen al cliente. Sin necesidad de useEffect ni estado.
generateStaticParams
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({
slug: post.slug,
}));
}generateStaticParams() pre-genera rutas dinámicas en el build. Retorna un array de params. Cada objeto genera una página estática. Sin esto, las rutas dinámicas son SSR por defecto.
Loading con Skeleton
// app/blog/loading.tsx
export default function Loading() {
return (
<div className="skeleton">
<div className="h-4 w-3/4 animate-pulse" />
<div className="h-4 w-1/2 animate-pulse" />
</div>
);
}loading.tsx se envuelve automáticamente en Suspense. Muestra skeleton/placeholder durante el data fetching. Cada sección puede tener su propio loading para granularidad.
SSG (Estático)
// Generado en el build (estático)
export const dynamic = "force-static";
export default async function Page() {
const data = await fetch(url);
return <p>{data.title}</p>;
}force-static genera la página en el build (SSG). El HTML se sirve desde CDN sin computación. Ideal para contenido que no cambia. Por defecto, las páginas sin datos dinámicos ya son estáticas.
revalidatePath
"use server";
import { revalidatePath } from "next/cache";
export async function publish() {
await db.save(post);
revalidatePath("/blog");
revalidatePath("/");
}revalidatePath() invalida la cache de una ruta manualmente. La siguiente visita regenera la página. Úsalo en Server Actions tras mutaciones para garantizar datos frescos.
unstable_cache
import { unstable_cache } from "next/cache";
const getCachedPosts = unstable_cache(
async () => db.posts.findMany(),
["posts"],
{ revalidate: 3600, tags: ["posts"] }
);
const posts = await getCachedPosts();unstable_cache hace cache de cualquier función async (no solo fetch). Acepta clave, revalidate y tags. Útil para queries de BD o cálculos pesados que no necesitan fetch HTTP.
ISR (Revalidación)
// Revalidar cada 60 segundos
export const revalidate = 60;
// O por fetch:
const res = await fetch(url, {
next: { revalidate: 3600 }
});revalidate activa ISR (Incremental Static Regeneration). La página es estática pero se regenera en background cada N segundos. Combina el rendimiento de SSG con datos actualizados.
revalidateTag
// Fetch con tag:
fetch(url, { next: { tags: ["posts"] } })
// Invalidar por tag:
import { revalidateTag } from "next/cache";
revalidateTag("posts");tags agrupan fetches relacionados. revalidateTag() invalida todos los fetches con esa tag de una vez. Más granular que revalidatePath cuando múltiples páginas usan los mismos datos.
Cache del Fetch
// Cache indefinida (default en producción)
fetch(url, { cache: "force-cache" })
// Sin cache (siempre fresco)
fetch(url, { cache: "no-store" })
// Revalidar por tiempo
fetch(url, { next: { revalidate: 60 } })force-cache guarda la respuesta indefinidamente. no-store búsqueda siempre datos frescos (equivalente a SSR). revalidate hace cache con expiración temporal.
Streaming con Suspense
import { Suspense } from "react";
export default function Page() {
return (
<div>
<h1>Blog</h1>
<Suspense fallback={<p>Cargando...</p>}>
<PostList />
</Suspense>
</div>
);
}Suspense permite streaming: envía el HTML shell inmediatamente y el contenido cuando está listo. El fallback se muestra mientras carga. Mejora TTFB y UX percibida.
Routing e Navegação
Ruta Dinámica
// app/blog/[slug]/page.tsx
export default function Post({
params,
}: { params: { slug: string } }) {
return <h1>{params.slug}</h1>;
}Los corchetes [slug] crean segmentos dinámicos. El valor queda en params.slug. /blog/hola-mundo → params.slug = "hola-mundo".
redirect
import { redirect } from "next/navigation";
export default async function Page() {
const user = await getUser();
if (!user) {
redirect("/login");
}
return <p>Hola, {user.name}</p>;
}redirect() redirige en el servidor (HTTP 307). Lanza una excepción internamente — no necesita return después. Úsalo para proteger rutas o redirigir tras acciones.
Rewrites y Redirects
// next.config.js
module.exports = {
async rewrites() {
return [{ source: "/api/:path*",
destination: "https://api.external.com/:path*" }];
},
async redirects() {
return [{ source: "/viejo",
destination: "/nuevo", permanent: true }];
},
};rewrites hace proxy de requests sin cambiar la URL (transparente). redirects envía HTTP 301/308. Configurados en next.config.js. Útiles para migraciones y APIs externas.
Catch-all Routes
// app/docs/[...slug]/page.tsx // /docs/a/b/c → params.slug = ["a", "b", "c"] // Optional catch-all: // app/docs/[[...slug]]/page.tsx // /docs → params.slug = undefined
[...slug] captura múltiples segmentos como array. [[...slug]] (dobles corchetes) es opcional — también corresponde a la ruta sin segmentos.
notFound()
import { notFound } from "next/navigation";
export default async function Post({ params }) {
const post = await getPost(params.slug);
if (!post) {
notFound();
}
return <h1>{post.title}</h1>;
}notFound() renderiza el not-found.tsx más cercano. Lanza una excepción interna (como redirect). Úsalo cuando un recurso no existe en la BD para retornar 404 correctamente.
useRouter
"use client";
import { useRouter } from "next/navigation";
export function Button() {
const router = useRouter();
return (
<button onClick={() => router.push("/login")}>
Entrar
</button>
);
}useRouter() permite navegación programática en Client Components. push() añade al historial. replace() sustituye. back() vuelve. refresh() re-ejecuta el load.
Link con Prefetch
import Link from "next/link";
<Link href="/dashboard" prefetch={true}>
Dashboard
</Link>
<Link href="/heavy" prefetch={false}>
Página pesada
</Link>prefetch carga la página en background cuando el link es visible. Por defecto es automático para links estáticos. prefetch={false} lo desactiva para páginas pesadas o autenticadas.
usePathname y useSearchParams
"use client";
import { usePathname, useSearchParams } from "next/navigation";
export function Filter() {
const pathname = usePathname(); // "/blog"
const searchParams = useSearchParams();
const page = searchParams.get("page"); // "2"
}usePathname() retorna la ruta actual. useSearchParams() da acceso a los query params. Ambos reactivos — se actualizan cuando la URL cambia. Solo en Client Components.
Route Handlers Dinámicos
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = await getPost(params.id);
return Response.json(post);
}Los route handlers en carpetas dinámicas reciben params como segundo argumento. request es el objeto Request nativo. Soporta todos los verbos HTTP exportados.
API e Server Actions
Route Handler GET
// app/api/users/route.ts
export async function GET() {
const users = await db.users.findMany();
return Response.json(users);
}route.ts crea un endpoint API. Exporta funciones con nombres de verbos: GET, POST, PUT, DELETE. Response.json() retorna JSON con los headers correctos.
Usar Server Action en Form
import { createPost } from "./actions";
export default function Form() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">Crear</button>
</form>
);
}action={serverAction} conecta el form directamente. Funciona sin JavaScript (progressive enhancement). El form hace POST automático y la action procesa en el servidor.
Streaming Responses
export async function GET() {
const stream = new ReadableStream({
start(controller) {
controller.enqueue("Hola ");
setTimeout(() => {
controller.enqueue("Mundo!");
controller.close();
}, 1000);
},
});
return new Response(stream);
}Los route handlers soportan ReadableStream para respuestas en streaming. Útil para SSE, respuestas de AI o datos progresivos. El cliente recibe chunks a medida que se generan.
Route Handler POST
// app/api/users/route.ts
export async function POST(request: Request) {
const body = await request.json();
const user = await db.users.create(body);
return Response.json(user, { status: 201 });
}request.json() lee el body JSON. request.formData() para forms. El segundo argumento de Response.json() define status y headers personalizados.
useFormStatus y useActionState
"use client";
import { useFormStatus } from "react-dom";
import { useActionState } from "react";
function Submit() {
const { pending } = useFormStatus();
return <button disabled={pending}>
{pending ? "Enviando..." : "Enviar"}
</button>;
}useFormStatus() da el estado del form (pending, data). useActionState() gestiona el retorno de la action (éxito/error). Ambos mejoran la UX durante los envíos.
CORS y Middleware API
export async function OPTIONS() {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}OPTIONS responde a los preflight requests CORS. Define headers Access-Control-* para permitir orígenes externos. Alternativa: configurar CORS en el middleware.ts global.
Query Params y Headers
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = searchParams.get("page");
const auth = request.headers.get("Authorization");
return Response.json({ page, auth });
}new URL(request.url) da acceso a searchParams. request.headers.get() lee headers. Ambos usan APIs web nativas (no específicas de Next.js).
cookies y headers
import { cookies, headers } from "next/headers";
// Leer cookie:
const token = cookies().get("token")?.value;
// Definir cookie:
cookies().set("theme", "dark", { path: "/" });
// Leer header:
const ua = headers().get("user-agent");cookies() y headers() acceden al request en el servidor. Funcionan en Server Components, Route Handlers y Server Actions. cookies().set() define cookies en la respuesta.
Server Action Básica
"use server";
// app/actions.ts
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
await db.posts.create({ title });
}"use server" al inicio marca el archivo como Server Actions. Las funciones corren solo en el servidor. Reciben FormData o argumentos serializables. Sustituyen endpoints API para mutaciones.
revalidatePath en Action
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function create(formData: FormData) {
await db.posts.create({ title: formData.get("title") });
revalidatePath("/blog");
redirect("/blog");
}Tras una mutación, revalidatePath() invalida la cache y redirect() navega. Patrón completo: guardar → invalidar → redirigir. El usuario ve datos frescos inmediatamente.
Renderização
Server Component (Default)
// app/page.tsx (sin directiva = Server)
export default async function Page() {
const data = await fetch(url);
return <div>{data.title}</div>;
}Por defecto, todos los componentes son Server Components. Pueden ser async, acceder a la BD y usar secretos. Cero JavaScript enviado al cliente — renderizados en el servidor.
dynamic import
import dynamic from "next/dynamic";
const Map = dynamic(() => import("./Map"), {
ssr: false,
loading: () => <p>Cargando mapa...</p>,
});dynamic() hace code-splitting y lazy loading. ssr: false desactiva la renderización en el servidor (para libs que usan window/document). loading muestra un fallback.
Hydration y Errores
"use client";
import { useEffect, useState } from "react";
export function Time() {
const [time, setTime] = useState("");
useEffect(() => {
setTime(new Date().toLocaleTimeString());
}, []);
return <p>{time}</p>;
}El hydration mismatch ocurre cuando el HTML del servidor difiere del render del cliente. Valores como Date.now() o Math.random() causan errores. Usa useEffect para valores solo del cliente.
Client Component
"use client";
import { useState } from "react";
export default function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}"use client" al inicio lo marca como Client Component. Necesario para hooks (useState, useEffect), eventos (onClick) y APIs del navegador. Se ejecuta en cliente y servidor (hydration).
Suspense para Streaming
import { Suspense } from "react";
export default function Page() {
return (
<>
<h1>Dashboard</h1>
<Suspense fallback={<Skeleton />}>
<SalesChart />
</Suspense>
</>
);
}Suspense envía el HTML disponible inmediatamente y hace stream del resto. Cada Suspense es independiente — múltiples pueden resolverse en paralelo. Mejora el TTFB significativamente.
Componer Server + Client
// app/page.tsx (Server)
import Counter from "./Counter";
export default async function Page() {
const data = await fetch(url);
return (
<div>
<h1>{data.title}</h1>
<Counter initial={data.value} />
</div>
);
}Patrón recomendado: el Server búsqueda datos y los pasa como props a los Client Components. Mantiene la lógica pesada en el servidor. El Client solo recibe datos serializables y gestiona la interactividad.
Renderización Estática vs Dinámica
// Forzar estático: export const dynamic = "force-static"; // Forzar dinámico (SSR): export const dynamic = "force-dynamic"; // Runtime: export const runtime = "edge";
dynamic controla cuándo se genera la página. force-static = build time. force-dynamic = cada request. runtime = "edge" ejecuta en edge (Vercel Edge, Cloudflare Workers).
children como Composición
"use client";
// Modal.tsx
export function Modal({ children }: {
children: React.ReactNode
}) {
const [open, setOpen] = useState(true);
return open ? <div>{children}</div> : null;
}
// El Server puede pasar children:
<Modal><ServerContent /></Modal>children permite Server Components dentro de Client Components. El contenido se renderiza en el servidor y se "proyecta" en el cliente. Mantén el árbol Server lo más arriba posible.
Partial Prerendering (PPR)
// next.config.js
experimental: { ppr: true }
// app/page.tsx
import { Suspense } from "react";
export default function Page() {
return (
<>
<Header /> {/* estático */}
<Suspense fallback={<Skeleton />}>
<Profile /> {/* dinámico */}
</Suspense>
</>
);
}PPR (experimental) combina estático y dinámico en la misma página. El shell se sirve desde CDN instantáneamente. Las partes dinámicas en Suspense se hacen streaming desde el servidor.
Avançado e Deploy
Middleware
// middleware.ts (raíz o src/)
import { NextResponse } from "next/server";
export function middleware(request: Request) {
const token = request.cookies.get("token");
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};middleware.ts intercepta requests antes de llegar a la ruta. matcher limita dónde corre. Úsalo para auth, redirects, A/B testing, geolocalización. Corre en el edge (rápido).
Deploy en Vercel
npm i -g vercel vercel # deploy preview vercel --prod # deploy producción # O: push a GitHub # Vercel lo detecta y hace deploy automático
Vercel es la plataforma nativa de Next.js. Deploy automático en cada push. Preview deployments por PR. Edge functions, ISR e image optimization incluidos. Cero configuración.
Testing (Jest + RTL)
// __tests__/page.test.tsx
import { render, screen } from "@testing-library/react";
import Home from "@/app/page";
test("muestra título", () => {
render(<Home />);
expect(screen.getByText("Bienvenido")).toBeTruthy();
});@testing-library/react renderiza componentes en tests. render() monta en el DOM virtual. screen.getByText() búsqueda elementos. Para Server Components, usa jest-environment-jsdom.
Edge Runtime
// app/api/geo/route.ts
export const runtime = "edge";
export async function GET(request: Request) {
const geo = request.geo;
return Response.json({
country: geo?.country,
city: geo?.city,
});
}runtime = "edge" ejecuta en edge locations (más cerca del usuario). Sin acceso a APIs de Node.js (fs, net). Ideal para respuestas rápidas: geo, auth, redirects, A/B tests.
Docker (standalone)
# Dockerfile FROM node:20-alpine AS builder WORKDIR /app COPY . . RUN npm ci && npm run build FROM node:20-alpine AS runner WORKDIR /app COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/public ./public CMD ["node", "server.js"]
output: "standalone" genera un servidor Node mínimo. El Dockerfile multi-stage copia solo lo necesario. Imagen final ~50MB. Ideal para VPS, Kubernetes, AWS ECS.
Performance y Core Web Vitals
// app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
// Monitorizar:
export function reportWebVitals(metric) {
console.log(metric.name, metric.value);
}
// next build muestra:
// Route Size First Load JSNext.js optimiza automáticamente: code-splitting, image optimization, font optimization. next build muestra el tamaño por ruta. reportWebVitals monitoriza LCP, FID, CLS en producción.
Dynamic Imports y Lazy
import dynamic from "next/dynamic";
// Sin SSR (solo cliente):
const Editor = dynamic(() => import("./Editor"), {
ssr: false,
});
// Con loading:
const Chart = dynamic(() => import("./Chart"), {
loading: () => <p>Cargando...</p>,
});dynamic() divide el bundle y carga bajo demanda. ssr: false para libs que necesitan window/document. Reduce el JavaScript inicial — mejora el First Contentful Paint.
Static Export
// next.config.js
module.exports = {
output: "export",
};
// Build:
npm run build
// Genera la carpeta out/ con HTML estáticooutput: "export" genera un sitio 100% estático (sin servidor Node). HTML/CSS/JS en la carpeta out/. Sin SSR, API routes ni middleware. Ideal para GitHub Pages, S3, Netlify.
next.config Avanzado
module.exports = {
output: "standalone",
experimental: {
serverActions: { bodySizeLimit: "2mb" },
},
headers: async () => [{
source: "/(.*)",
headers: [{ key: "X-Frame-Options", value: "DENY" }],
}],
};output: "standalone" genera un build mínimo para Docker. headers añade headers de seguridad globales. experimental activa features experimentales. Configura según las necesidades de deploy.
Instrumentation y Logging
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./sentry.server.config");
}
if (process.env.NEXT_RUNTIME === "edge") {
await import("./sentry.edge.config");
}
}instrumentation.ts corre una vez al arrancar el servidor. Ideal para inicializar Sentry, OpenTelemetry o conexiones a BD. NEXT_RUNTIME distingue Node.js de Edge.
Estilização e Assets
CSS Modules
// Card.module.css
.card { padding: 1rem; }
.title { font-size: 1.5rem; }
// Card.tsx
import styles from "./Card.module.css";
<div className={styles.card}>
<h2 className={styles.title}>{title}</h2>
</div>CSS Modules hacen scope automático por componente. Los nombres de clase son únicos (hash). Sin conflictos globales. Los archivos .module.css tienen soporte nativo.
next/image Avanzado
import Image from "next/image";
<Image
src="https://example.com/photo.jpg"
fill
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: "cover" }}
placeholder="blur"
blurDataURL="data:image/..."
/>fill para imágenes responsivas (el padre necesita position: relative). sizes optimiza el srcset. placeholder="blur" muestra blur-up mientras carga. Los dominios externos deben estar en images.domains.
CSS-in-JS (styled-components)
// lib/registry.tsx
"use client";
import { ServerStyleSheet } from "styled-components";
// next.config.js:
compiler: { styledComponents: true }
// Componente:
const Title = styled.h1`
color: tomato;
font-size: 2rem;
`;Next.js soporta styled-components con compiler.styledComponents: true. Requiere registry para SSR. Alternativas: Emotion, Vanilla Extract. Tailwind es la opción más popular.
Tailwind CSS
// globals.css @tailwind base; @tailwind components; @tailwind utilities; // Componente: <div className="p-4 rounded-lg shadow-md"> <h2 className="text-xl font-bold">Título</h2> </div>
Tailwind CSS viene como opción en create-next-app. Clases utilitarias inline. tailwind.config.ts personaliza el tema. Purge automático en el build — solo se incluye el CSS usado.
Archivos Estáticos
public/ logo.png favicon.ico robots.txt // Uso: <img src="/logo.png" /> // Servido en: https://site.com/logo.png
La carpeta public/ sirve archivos en la raíz del sitio. Sin procesamiento — se copian tal cual. Para imágenes, prefiere next/image (optimización). Ideal para robots.txt, favicons, SVGs.
CSS Global
// app/globals.css
* { box-sizing: border-box; }
body { margin: 0; font-family: sans-serif; }
// Importar en el layout raíz:
// app/layout.tsx
import "./globals.css";El CSS global solo puede importarse en app/layout.tsx (raíz). Se aplica a todas las páginas. Para estilos por componente, usa CSS Modules o Tailwind. No importes CSS global en sub-componentes.
SVG como Componente
// next.config.js
module.exports = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ["@svgr/webpack"],
});
return config;
},
};
// Uso:
import Logo from "./logo.svg";
<Logo width={48} />@svgr/webpack permite importar SVGs como componentes React. Acepta props como width, fill. Alternativa: importar como URL con next/image.
Styled JSX
export function Card() {
return (
<div>
<p>Hola</p>
<style jsx>{`
p { color: blue; font-size: 16px; }
`}</style>
</div>
);
}styled-jsx viene incluido en Next.js. El CSS dentro de <style jsx> tiene scope al componente. Soporta global para estilos sin scope. Alternativa ligera a CSS Modules.
Dark Mode con Tailwind
// tailwind.config.ts darkMode: "class", // layout.tsx: <html className="dark"> // Componente: <div className="bg-white dark:bg-gray-900"> <p className="text-black dark:text-white">Hola</p> </div>
darkMode: "class" activa dark mode por clase en el <html>. El prefijo dark: aplica estilos en modo oscuro. Combínalo con next-themes para toggle con persistencia.