Preguntas de entrevista de Next.js con respuestas (35+)
Next.js se volvió el estándar de facto para aplicaciones React en producción. Si estás aplicando a un rol de frontend o full-stack, vas a encontrarte con al menos una de estas preguntas en la técnica. Esta guía cubre las 40+ preguntas que más se repiten — desde conceptos base hasta arquitectura avanzada — con respuestas directas y código real para que puedas responderlas con confianza.
Fundamentos de Next.js
1. ¿Qué es Next.js y en qué se diferencia de React puro?
Respuesta: Next.js es un framework de React que agrega rendering del lado del servidor (SSR), generación de sitios estáticos (SSG), routing basado en archivos, optimización de imágenes, y manejo de API routes — todo sin configuración. React por sí solo es solo una librería de UI: te da el renderizado de componentes pero nada de routing, SSR ni build pipeline. Next.js resuelve la capa de "aplicación" encima de React.
Lo que diferencia a los candidatos fuertes: mencionan el tradeoff — Next.js agrega opinión (estructura de carpetas, convenciones de naming, hydration automática) que acelera el desarrollo pero limita la flexibilidad si el equipo tiene necesidades muy específicas de build.
2. Explicá las diferencias entre Pages Router y App Router
Respuesta directa:
| Característica | Pages Router | App Router |
|----------------|-------------|------------|
| Directorio base | pages/ | app/ |
| Server Components | No | Sí (por defecto) |
| Layouts anidados | Manual | Nativo (layout.tsx) |
| Data fetching | getServerSideProps, getStaticProps | async/await directo en componentes |
| Streaming/Suspense | No | Sí |
| Cache granular | No | Sí (por fetch) |
App Router (Next.js 13+) adopta el modelo de React Server Components: los componentes son server-first por defecto y solo marcás con "use client" cuando necesitás interactividad. Esto reduce drásticamente el JavaScript enviado al cliente.
Cuándo usar Pages Router todavía: proyectos legacy, equipos que no pueden absorber la curva de aprendizaje del modelo server/client, o librerías de terceros que aún no son compatibles con RSC.
3. ¿Qué es el Server-Side Rendering (SSR) y cuándo lo usarías?
Respuesta: En SSR, cada request genera el HTML en el servidor — el usuario recibe HTML completo (favorable para SEO y primer pintado) en lugar de HTML vacío + JS que hidrata. En Next.js con Pages Router se implementa con getServerSideProps; en App Router, cualquier Server Component que haga fetch sin cache: 'force-cache' efectivamente hace SSR.
Usarlo cuando:
- El contenido cambia con cada request (datos del usuario, inventario en tiempo real, precios dinámicos)
- SEO es crítico y los datos no pueden pre-generarse
- Necesitás acceder a cookies o headers del request para personalizar
No usarlo cuando: el contenido es estático o se puede revalidar periódicamente — SSR tiene costo de latencia en cada request.
// Pages Router — SSR clásico
export async function getServerSideProps(context) {
const { params, req } = context;
const data = await fetchUserDashboard(params.id, req.cookies.token);
return { props: { data } };
}4. ¿Qué es la Static Site Generation (SSG) y cuándo conviene sobre SSR?
Respuesta: En SSG, las páginas se generan en tiempo de build como HTML estático. El resultado se sirve desde CDN — latencia mínima, sin carga en el servidor por request. En Pages Router: getStaticProps. En App Router: fetch con cache: 'force-cache' (default) o generateStaticParams.
SSG conviene cuando:
- El contenido no cambia por usuario (posts de blog, docs, landing pages, páginas de producto)
- El volumen de páginas es manejable en build time
- Querés el mejor tiempo de carga posible
El problema con SSG puro: si el contenido cambia frecuentemente, el sitio queda desactualizado hasta el próximo build. La solución es ISR.
// Pages Router — SSG con revalidación
export async function getStaticProps() {
const posts = await fetchPosts();
return {
props: { posts },
revalidate: 60, // revalida cada 60s (ISR)
};
}5. ¿Qué es Incremental Static Regeneration (ISR)?
Respuesta: ISR combina lo mejor de SSG y SSR. Las páginas se generan estáticamente pero pueden regenerarse en background después de un tiempo de expiración, sin rebuild completo. El primer request después del TTL sirve la versión en caché (stale) y dispara una regeneración; el siguiente request ya ve la versión nueva.
En App Router, ISR se configura por fetch:
// App Router — ISR granular por fetch
async function ProductPage({ params }) {
const product = await fetch(`/api/products/${params.id}`, {
next: { revalidate: 3600 }, // revalida cada hora
}).then(r => r.json());
return <Product data={product} />;
}O con revalidation on-demand (mejor para CMS):
// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
export async function POST(req: Request) {
const { path } = await req.json();
revalidatePath(path);
return Response.json({ revalidated: true });
}6. ¿Cómo funciona el routing en el App Router?
Respuesta: El App Router usa el sistema de archivos como fuente de verdad. Cada carpeta dentro de app/ define un segmento de ruta. Los archivos especiales determinan el comportamiento:
| Archivo | Propósito |
|---------|-----------|
| page.tsx | UI pública de la ruta |
| layout.tsx | UI compartida que envuelve hijos (persiste entre navegaciones) |
| loading.tsx | UI de loading con Suspense automático |
| error.tsx | Error boundary para el segmento |
| not-found.tsx | Render cuando notFound() se llama |
| route.ts | API endpoint (equivalente a API routes) |
Rutas dinámicas: app/blog/[slug]/page.tsx → /blog/mi-post.
Grupos de rutas (sin impacto en URL): app/(marketing)/about/page.tsx → /about.
Rutas paralelas: app/@modal/(.)producto/[id]/page.tsx para modales que preservan la URL.
7. ¿Qué son los React Server Components (RSC) y qué los hace distintos?
Respuesta: Los RSC se renderizan exclusivamente en el servidor — su código nunca llega al bundle del cliente. Pueden:
- Acceder directamente a bases de datos, sistemas de archivos, servicios internos
- Usar
async/awaitsinuseEffectni estado de carga - Reducir el JavaScript enviado al cliente a cero para esos componentes
Lo que NO pueden hacer: usar hooks de React (useState, useEffect, etc.), acceder a APIs del browser (window, document), agregar event listeners.
// Server Component — corre solo en servidor
// NO necesitás "use client"
async function UserProfile({ userId }: { userId: string }) {
// Esto corre en el servidor, nunca en el browser
const user = await db.users.findUnique({ where: { id: userId } });
return (
<div>
<h1>{user.name}</h1>
{/* InteractiveButton sí necesita "use client" */}
<InteractiveButton userId={userId} />
</div>
);
}Regla práctica: empezá todo como Server Component. Agregá "use client" solo cuando necesitás interactividad, estado local o APIs del browser.
8. ¿Cuándo usás `"use client"` y cuándo no lo necesitás?
Respuesta: "use client" crea un "boundary" entre server y client. Todo lo que está dentro del componente marcado (y sus hijos directos) corre en el cliente.
Necesitás "use client" cuando:
- Usás
useState,useReducer,useEffect,useRef - Manejas eventos del browser (
onClick,onChange,onSubmit) - Usás
window,document,localStorage - Usás librerías que requieren DOM (animaciones, drag-and-drop, mapas)
No necesitás "use client" cuando:
- El componente solo renderiza UI sin interactividad
- Hace fetch de datos (usá
async/awaitdirectamente) - Accede a la DB o servicios del servidor
Error común: marcar toda la app como "use client" por comodidad. Esto anula los beneficios de RSC — el bundle crece y se pierde el acceso directo al servidor.
Data Fetching y Caché
9. ¿Cómo funciona el sistema de caché de Next.js 14/15?
Respuesta: Next.js tiene cuatro capas de caché:
- 1Request Memoization —
fetchcon la misma URL+opciones en el mismo render tree se deduplica automáticamente (in-memory, solo durante la request) - 2Data Cache — resultados de
fetchpersisten entre requests y deployments (se puede optar out concache: 'no-store') - 3Full Route Cache — HTML + RSC payload estático cacheado en el servidor
- 4Router Cache — prefetch de segmentos en el cliente durante la sesión del browser
// Datos frescos en cada request (SSR behavior)
const data = await fetch('/api/data', { cache: 'no-store' });
// Datos estáticos cacheados indefinidamente
const config = await fetch('/api/config', { cache: 'force-cache' });
// ISR — revalida cada 10 minutos
const posts = await fetch('/api/posts', { next: { revalidate: 600 } });Next.js 15 cambió el default: fetch ya NO cachea por defecto (breaking change vs 14). Ahora necesitás optar IN explícitamente con force-cache si querés cacheo.
10. ¿Qué es `unstable_cache` y cuándo lo usarías?
Respuesta: unstable_cache (ahora cache en Next.js 15 RC) permite cachear el resultado de cualquier función asíncrona — no solo fetch. Útil cuando querés cachear queries de ORM (Prisma, Drizzle), llamadas a SDKs de terceros, o cualquier operación costosa.
import { unstable_cache } from "next/cache";
const getUser = unstable_cache(
async (userId: string) => {
return await prisma.user.findUnique({ where: { id: userId } });
},
["user-by-id"], // cache key prefix
{
revalidate: 300, // 5 minutos
tags: ["user"], // permite revalidar por tag
}
);
// En tu componente
const user = await getUser(params.id);Invalidar por tag (on-demand revalidation):
import { revalidateTag } from "next/cache";
// Después de actualizar un usuario:
revalidateTag("user");11. Diferencia entre `revalidatePath` y `revalidateTag`
Respuesta:
| | revalidatePath | revalidateTag |
|--|--|--|
| Granularidad | Por URL/path | Por tag (puede afectar muchas rutas) |
| Uso típico | "Actualicé el post con slug X" | "Actualizé cualquier post" |
| Efecto | Purga esa ruta específica | Purga todos los fetches con ese tag |
// Después de editar un post específico
revalidatePath(`/blog/${slug}`);
// Después de cualquier cambio en posts
revalidateTag("blog-posts");
// Todos los fetches con { next: { tags: ['blog-posts'] } } se invalidanServer Actions y Mutations
12. ¿Qué son los Server Actions?
Respuesta: Los Server Actions son funciones async que corren en el servidor pero se pueden llamar desde Client Components (o formularios HTML). Eliminan la necesidad de crear API routes para mutaciones simples — la lógica de servidor se define inline o en archivos separados con "use server".
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
// Directamente a la DB — sin API route
await db.posts.create({ data: { title } });
revalidatePath("/blog");
}// Client Component que lo usa
"use client";
import { createPost } from "./actions";
export function PostForm() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Crear post</button>
</form>
);
}Puntos clave para la entrevista: los Server Actions se envían al servidor vía HTTP POST (no WebSockets); "use server" puede estar al top del archivo o como directiva inline dentro de una función; siempre validar inputs del servidor con Zod o similar, nunca confiar en lo que llega del cliente.
13. ¿Cómo manejás el estado de una Server Action (pending, error, success)?
Respuesta: Con el hook useActionState (antes useFormState en versiones < 19):
"use client";
import { useActionState } from "react";
import { createPost } from "./actions";
type State = { error?: string; success?: boolean };
export function PostForm() {
const [state, action, isPending] = useActionState<State, FormData>(
createPost,
{}
);
return (
<form action={action}>
<input name="title" disabled={isPending} />
{state.error && <p className="text-red-500">{state.error}</p>}
{state.success && <p>¡Post creado!</p>}
<button type="submit" disabled={isPending}>
{isPending ? "Creando..." : "Crear"}
</button>
</form>
);
}// La action ahora recibe state previo + formData
"use server";
export async function createPost(
prevState: State,
formData: FormData
): Promise<State> {
const title = formData.get("title") as string;
if (!title) return { error: "El título es requerido" };
try {
await db.posts.create({ data: { title } });
revalidatePath("/blog");
return { success: true };
} catch {
return { error: "Error al crear el post" };
}
}Optimización y Performance
14. ¿Cómo funciona `next/image` y por qué es importante?
Respuesta: El componente de Next.js hace optimización automática:
- Convierte imágenes a WebP/AVIF según soporte del browser
- Sirve el tamaño exacto según el viewport (responsive)
- Lazy loading nativo por defecto
- Previene Cumulative Layout Shift (CLS) requiriendo
widthyheight - Optimización bajo demanda (no en build time) — ahorra tiempo de build
import Image from "next/image";
// Imagen local (Next.js infiere el tamaño)
import hero from "@/public/hero.png";
<Image src={hero} alt="Hero" priority />
// Imagen remota (requiere config en next.config)
<Image
src="https://cdn.example.com/product.jpg"
alt="Producto"
width={800}
height={600}
sizes="(max-width: 768px) 100vw, 800px"
/>priority prop: para imágenes above the fold — bypasa el lazy loading y mejora LCP (Largest Contentful Paint).
Error común en entrevistas: no configurar images.remotePatterns en next.config para imágenes externas — Next.js bloquea dominios no autorizados por seguridad.
15. ¿Qué es `next/font` y cómo optimiza la performance?
Respuesta: next/font descarga las fuentes en build time, las aloja en tu dominio (cero requests a Google Fonts en runtime), y genera automáticamente el CSS de font-display: swap con size-adjust para prevenir layout shift.
// app/layout.tsx
import { Inter, Geist_Mono } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
display: "swap",
});
export default function RootLayout({ children }) {
return (
<html className={inter.variable}>
<body>{children}</body>
</html>
);
}Fuente local:
import localFont from "next/font/local";
const myFont = localFont({ src: "./fonts/MyFont.woff2" });Por qué importa para Core Web Vitals: elimina el CLS de fuentes externas y el FOIT/FOUT — impacto directo en LCP y CLS scores.
16. ¿Cómo implementás code splitting y lazy loading en Next.js?
Respuesta: Next.js hace code splitting automático por ruta. Para lazy loading manual:
import dynamic from "next/dynamic";
// Componente cargado solo cuando se renderiza
const HeavyChart = dynamic(() => import("./HeavyChart"), {
loading: () => <p>Cargando gráfico...</p>,
ssr: false, // útil para librerías que requieren browser (window, document)
});
// Con named export
const Modal = dynamic(() =>
import("./Modal").then(mod => mod.Modal)
);Casos de uso para ssr: false:
- Librerías de mapas (Leaflet, Mapbox)
- Editores de texto ricos (TipTap, Quill, Monaco)
- Componentes que usan
windowodocumentdirectamente
En App Router: usás React.lazy + Suspense directamente — next/dynamic sigue funcionando pero no es la única opción.
17. ¿Cómo optimizás el bundle size en Next.js?
Respuesta:
- 1Analizar con
@next/bundle-analyzer:
npm install @next/bundle-analyzer
ANALYZE=true npm run build- 2Server Components primero — código que no necesita ser interactivo no va al bundle del cliente.
- 3Imports con barrel files — evitá
import { todo } from 'libreria-gigante'; preferí imports específicos:
// Malo — importa toda la librería
import { format } from 'date-fns';
// Bien — import directo del módulo
import format from 'date-fns/format';- 4
next/dynamicconssr: falsepara componentes pesados solo-cliente.
- 5Revisar dependencias — lodash vs lodash-es, moment vs date-fns, etc.
- 6
experimental.optimizePackageImportsennext.config:
experimental: {
optimizePackageImports: ['@mui/material', 'lucide-react'],
}Middleware y Autenticación
18. ¿Para qué sirve el middleware en Next.js?
Respuesta: El middleware corre en el Edge Runtime antes de que la request llegue a la página o API route. Se usa para:
- Autenticación/autorización — redirigir a
/loginsi no hay sesión - Internacionalización — detectar idioma y redirigir
- A/B testing — asignar variante basado en cookie o geo
- Rewrite de URLs — proxies, legacy URL handling
- Rate limiting — bloquear bots o abusos
// middleware.ts (en la raíz del proyecto)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("auth-token");
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/api/protected/:path*"],
};Limitaciones del Edge Runtime: no podés usar Node.js APIs (fs, crypto nativo, módulos nativos). Usás las Web APIs estándar.
19. ¿Cómo implementás autenticación en Next.js? ¿Qué opciones existen?
Respuesta:
Auth.js (NextAuth v5) — la opción más común para Next.js:
// auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
export const { auth, handlers, signIn, signOut } = NextAuth({
providers: [GitHub],
callbacks: {
authorized({ auth, request }) {
return !!auth?.user; // protege rutas
},
},
});Otras opciones populares:
- Clerk — autenticación como servicio, excelente DX, precios según MAU
- Supabase Auth — si ya usás Supabase, integración nativa con RLS
- Custom JWT — control total pero más código
Flujo de protección de rutas (App Router):
// middleware.ts
export { auth as middleware } from "@/auth";
export const config = { matcher: ["/dashboard/:path*"] };Lo que diferencia candidatos senior: hablan del manejo de tokens (HttpOnly cookies vs localStorage), refresh token rotation, CSRF protection, y las implicaciones de seguridad de cada elección.
API Routes y Edge Functions
20. ¿Qué son las Route Handlers y cómo difieren de las API Routes del Pages Router?
Respuesta: Las Route Handlers (App Router) usan la Web Request/Response API estándar en lugar de req/res del Node.js estilo Express.
// app/api/posts/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = searchParams.get("page") ?? "1";
const posts = await db.posts.findMany({
take: 10,
skip: (parseInt(page) - 1) * 10,
});
return NextResponse.json({ posts });
}
export async function POST(request: NextRequest) {
const body = await request.json();
// validar con Zod...
const post = await db.posts.create({ data: body });
return NextResponse.json(post, { status: 201 });
}Diferencia clave: Route Handlers pueden correr en Edge Runtime o Node.js runtime. Las API Routes del Pages Router son solo Node.js.
21. ¿Cuándo usarías Edge Runtime vs Node.js Runtime?
Respuesta:
| | Edge Runtime | Node.js Runtime |
|--|--|--|
| Cold start | ~0ms | ~100-300ms |
| Disponibilidad | 300+ regiones globales | Según provider |
| APIs disponibles | Web APIs, limitado | Todo Node.js |
| Acceso a DB | Solo databases Edge-compatible (Neon, PlanetScale) | Cualquier DB |
| fs, módulos nativos | No | Sí |
| Ideal para | Middleware, auth checks, redirects, A/B | API routes con DB, procesamiento pesado |
// Forzar Edge en una route específica
export const runtime = "edge";
// Forzar Node.js
export const runtime = "nodejs";Consejo para la entrevista: la elección correcta depende de si necesitás acceso a bases de datos tradicionales (usa Node.js) o solo lógica liviana con baja latencia (usa Edge).
Patrones Avanzados
22. ¿Qué es el patrón de composición de layouts en App Router?
Respuesta: En App Router, los layouts se anidan automáticamente. Cada layout.tsx envuelve a sus hijos y a los layouts de segmentos hijos:
app/
layout.tsx → RootLayout (html, body, nav global)
dashboard/
layout.tsx → DashboardLayout (sidebar, header de dashboard)
page.tsx → Dashboard home
analytics/
page.tsx → Analytics page (hereda ambos layouts)// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
);
}Beneficio clave: los layouts no se re-renderean al navegar entre rutas del mismo segmento — solo el page.tsx cambia. Esto preserva scroll position y estado local del layout.
23. ¿Cómo implementás parallel routes y intercepting routes?
Respuesta: Las parallel routes permiten renderizar múltiples páginas en el mismo layout simultáneamente (con @folder):
app/
layout.tsx
@team/page.tsx → renderiza en slot "team"
@analytics/page.tsx → renderiza en slot "analytics"// app/layout.tsx
export default function Layout({ children, team, analytics }) {
return (
<>
{children}
{team}
{analytics}
</>
);
}Intercepting routes permiten mostrar una ruta en contexto de otra (modales, drawers):
app/
foto/[id]/page.tsx → página completa de la foto
@modal/(.)foto/[id]/page.tsx → modal cuando se navega desde el feedCaso de uso clásico: galería de fotos estilo Instagram donde hacer click en una foto abre un modal con la URL actualizada, pero recargando la URL directa muestra la página completa.
24. ¿Cómo manejás errores en App Router?
Respuesta: App Router usa el sistema de Error Boundaries de React, pero simplificado con archivos convencionales:
// app/dashboard/error.tsx
"use client"; // Error boundaries DEBEN ser client components
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Algo salió mal</h2>
<p>{error.message}</p>
<button onClick={reset}>Reintentar</button>
</div>
);
}global-error.tsx (en app/) captura errores que rompen el root layout.
not-found.tsx se activa cuando llamás notFound() desde una page/layout:
import { notFound } from "next/navigation";
async function ProductPage({ params }) {
const product = await getProduct(params.id);
if (!product) notFound(); // muestra el not-found.tsx más cercano
return <Product data={product} />;
}25. ¿Cómo implementás internacionalización (i18n) en Next.js?
Respuesta: Con App Router, el patrón recomendado es routing basado en segmentos de idioma:
app/
[lang]/
layout.tsx
page.tsx
blog/page.tsx// middleware.ts — detecta idioma y redirige
import { match } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
const locales = ["es", "en"];
const defaultLocale = "es";
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const hasLocale = locales.some(l => pathname.startsWith(`/${l}`));
if (hasLocale) return;
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}Librerías populares: next-intl (la más adoptada con App Router), i18next, o el approach nativo con JSON files simples.
26. ¿Qué es Streaming y cómo funciona con Suspense en Next.js?
Respuesta: En lugar de esperar a que todos los datos carguen para enviar HTML, Streaming envía el HTML del shell primero y "streamea" el contenido de cada Suspense boundary a medida que sus datos están disponibles.
// app/dashboard/page.tsx — Server Component
import { Suspense } from "react";
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* Este se muestra inmediatamente */}
<QuickStats />
{/* Este streamea cuando sus datos cargan */}
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart /> {/* async Server Component */}
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders /> {/* async Server Component */}
</Suspense>
</div>
);
}Por qué es importante: en SSR clásico, si RevenueChart tarda 2 segundos, el usuario espera 2 segundos para ver TODA la página. Con Streaming, ve el layout y QuickStats inmediatamente — mejor Time to First Byte y UX percibida.
27. ¿Cómo funciona el prefetching en Next.js?
Respuesta: Next.js hace prefetch automático de rutas enlazadas con :
- En desarrollo: prefetch desactivado (por performance del dev server)
- En producción: cuando un
entra al viewport, Next.js prefetchea el RSC payload de esa ruta en background
// Prefetch desactivado explícitamente
<Link href="/pesado" prefetch={false}>Página pesada</Link>
// Prefetch programático
import { useRouter } from "next/navigation";
const router = useRouter();
// En un hover handler:
router.prefetch("/dashboard");Cómo funciona internamente: Next.js hace un fetch al endpoint de RSC (?_rsc=...) y cachea el payload en el Router Cache del cliente. La navegación posterior es instantánea porque el HTML ya está ahí.
28. ¿Qué es `generateMetadata` y cómo lo usás para SEO dinámico?
Respuesta: generateMetadata es una función async que genera metadata dinámica para cada ruta en App Router:
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const post = await fetchPost(params.slug);
if (!post) return { title: "Post no encontrado" };
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.coverImage, width: 1200, height: 630 }],
type: "article",
publishedTime: post.publishedAt,
},
twitter: {
card: "summary_large_image",
title: post.title,
},
alternates: {
canonical: `https://misitio.com/blog/${params.slug}`,
},
};
}Punto importante: generateMetadata y generateStaticParams se ejecutan en el servidor — podés hacer DB queries directamente.
29. ¿Cómo generás páginas estáticas dinámicamente con `generateStaticParams`?
Respuesta: generateStaticParams reemplaza a getStaticPaths del Pages Router. Le dice a Next.js qué rutas pre-generar en build time:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map(post => ({ slug: post.slug }));
}
// Esta función es llamada con cada slug retornado arriba
export default async function BlogPost({ params }) {
const post = await fetchPost(params.slug);
return <Article post={post} />;
}Comportamiento para slugs no pre-generados:
dynamicParams = true(default): se generan on-demand y se cacheandynamicParams = false: devuelve 404 si el slug no está en la lista
export const dynamicParams = false; // 404 para slugs no listados30. ¿Cómo testeas componentes en Next.js?
Respuesta: Testing en Next.js tiene capas:
Unit/Component tests con Vitest + Testing Library:
// __tests__/Button.test.tsx
import { render, screen } from "@testing-library/react";
import { Button } from "@/components/Button";
test("muestra el texto y dispara el handler", async () => {
const handler = vi.fn();
render(<Button onClick={handler}>Guardar</Button>);
await userEvent.click(screen.getByRole("button", { name: "Guardar" }));
expect(handler).toHaveBeenCalledOnce();
});Server Components — son más difíciles de unit-testear porque son async. La recomendación del equipo de Next.js es testearlos via E2E o integración.
E2E con Playwright:
// e2e/blog.spec.ts
import { test, expect } from "@playwright/test";
test("muestra el post", async ({ page }) => {
await page.goto("/blog/mi-post");
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
});Para testar API routes: usar next-test-api-route-handler o los Request/Response nativos directamente.
Deployment y Configuración
31. ¿Qué opciones de deployment tiene Next.js?
Respuesta:
| Opción | Cuándo usarla |
|--------|--------------|
| Vercel | Default, integración total, Edge Network, Analytics |
| Self-hosted Node.js | next start después de next build — control total |
| Static export (output: 'export') | Solo SSG, sin server — sirve en S3, Netlify, Cloudflare Pages |
| Docker | Orquestación con K8s, ambientes on-premise |
| Netlify / Cloudflare Pages | Alternativas a Vercel con Edge support |
Para static export:
// next.config.js
module.exports = { output: "export" };Restricciones del static export: no se puede usar SSR, middleware, API routes, ni Image Optimization del servidor.
Docker multi-stage (standalone output):
FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM node:20-alpine AS runner
COPY --from=builder /app/.next/standalone ./
EXPOSE 3000
CMD ["node", "server.js"]Con output: 'standalone' en next.config.js para generar un bundle minimizado.
32. ¿Cómo configurás variables de entorno en Next.js?
Respuesta: Next.js tiene un sistema de env vars con prefijos que determinan exposición:
| Variable | Disponible en |
|----------|--------------|
| NEXT_PUBLIC_* | Cliente y servidor (embebida en el bundle) |
| Sin prefijo | Solo servidor (nunca va al browser) |
# .env.local (no commitear)
DATABASE_URL=postgresql://... # solo servidor
NEXT_PUBLIC_API_URL=https://api.ejemplo.com # cliente + servidor
STRIPE_SECRET_KEY=sk_live_... # solo servidor — NUNCA public// En Server Component o API route:
const db = new Client(process.env.DATABASE_URL); // ok
// En Client Component:
const apiUrl = process.env.NEXT_PUBLIC_API_URL; // ok
const secret = process.env.STRIPE_SECRET_KEY; // UNDEFINED en cliente — correctoError crítico de seguridad: usar NEXT_PUBLIC_ para secretos (API keys, database URLs). Esos valores quedan expuestos en el bundle JS visible en el browser.
33. ¿Qué es el archivo `next.config.js` y qué configuraciones son más comunes?
/** @type {import('next').NextConfig} */
const nextConfig = {
// Imágenes de dominios externos
images: {
remotePatterns: [
{ protocol: "https", hostname: "cdn.example.com" },
],
},
// Rewrites — proxy a otra API sin exponer el dominio
async rewrites() {
return [
{ source: "/api/legacy/:path*", destination: "https://old-api.com/:path*" },
];
},
// Redirects (HTTP 301/302)
async redirects() {
return [
{ source: "/old-about", destination: "/about", permanent: true },
];
},
// Headers de seguridad
async headers() {
return [{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
],
}];
},
// Output para Docker
output: "standalone",
// Features experimentales
experimental: {
ppr: true, // Partial Prerendering (Next.js 15)
},
};34. ¿Qué es Partial Prerendering (PPR)?
Respuesta: PPR es una feature experimental de Next.js 15 que combina SSG y SSR en la misma ruta. El "shell" estático (layout, header, contenido que no depende de datos dinámicos) se sirve instantáneamente desde la CDN. Los "holes" dinámicos (datos del usuario, carrito, inventario) se streamean después.
// app/product/[id]/page.tsx
import { Suspense } from "react";
export default function ProductPage({ params }) {
return (
<div>
{/* Estático — sirve desde CDN inmediatamente */}
<ProductDescription id={params.id} />
{/* Dinámico — streamea después */}
<Suspense fallback={<PriceSkeleton />}>
<DynamicPrice id={params.id} />
</Suspense>
<Suspense fallback={<StockSkeleton />}>
<StockStatus id={params.id} />
</Suspense>
</div>
);
}Lo innovador: sin PPR tenías que elegir entre SSG (todo estático) o SSR (todo dinámico). PPR hace que la misma ruta sea parcialmente estática y parcialmente dinámica.
35. ¿Cuál es la diferencia entre `useRouter` del Pages Router y del App Router?
Respuesta: Son imports diferentes con APIs distintas:
// Pages Router
import { useRouter } from "next/router";
const router = useRouter();
router.push("/destino");
router.query.id; // acceso a params
// App Router
import { useRouter } from "next/navigation";
const router = useRouter();
router.push("/destino");
// Los params NO están en useRouter — usás useParams():
import { useParams } from "next/navigation";
const params = useParams(); // { id: "123" }Otros hooks del App Router:
usePathname()— pathname actual (reemplazarouter.pathname)useSearchParams()— query string (reemplazarouter.query)useParams()— segmentos dinámicos
Error común: mezclar imports entre routers. Si importás de "next/router" en un proyecto con App Router, vas a ver errores en runtime.
36. ¿Cómo implementás optimistic updates con Server Actions?
Respuesta: Con el hook useOptimistic de React 19:
"use client";
import { useOptimistic } from "react";
import { toggleLike } from "./actions";
export function LikeButton({ post }: { post: Post }) {
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
post.likes,
(current, increment: number) => current + increment
);
return (
<form
action={async () => {
setOptimisticLikes(post.liked ? -1 : 1); // UI actualiza inmediatamente
await toggleLike(post.id); // server action en background
}}
>
<button type="submit">
{optimisticLikes} likes
</button>
</form>
);
}Si la server action falla, useOptimistic revierte al valor original automáticamente. Esto es lo que diferencia optimistic updates bien hechos — el manejo del rollback es automático.
37. ¿Cómo protegés rutas en App Router sin middleware?
Respuesta: Además del middleware, podés proteger rutas directamente en los layouts o pages:
// app/dashboard/layout.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function DashboardLayout({ children }) {
const session = await auth();
if (!session?.user) {
redirect("/login");
}
return (
<div>
<DashboardNav user={session.user} />
{children}
</div>
);
}Cuándo usar layout vs middleware:
- Middleware: protección perimetral, más rápido (Edge Runtime), ideal cuando la check es simple (¿existe el token?)
- Layout: validaciones más complejas con la DB (¿tiene el rol correcto? ¿suscripción activa?), acceso al contexto del usuario real
Importante: estas dos capas se complementan. El middleware bloquea a quienes claramente no tienen sesión; el layout valida permisos granulares.
38. ¿Qué diferencia hay entre `redirect` y `permanentRedirect` en App Router?
Respuesta:
import { redirect, permanentRedirect } from "next/navigation";
// 307 Temporary Redirect — el destino puede cambiar
redirect("/nueva-pagina");
// 308 Permanent Redirect — Google y browsers cachean esto
permanentRedirect("/nueva-url-definitiva");redirect (307): usar para redirects dinámicos basados en estado (autenticación, permisos, flujo de onboarding).
permanentRedirect (308): usar para cambios de URL definitivos (migración de /blog/vieja-url → /blog/nueva-url). Google transfiere PageRank. Los browsers recuerdan y ya no hacen la request.
En middleware: usar NextResponse.redirect() con el status code explícito.
39. ¿Cómo depurás problemas de hidratación en Next.js?
Respuesta: Los errores de hidratación ocurren cuando el HTML generado por el servidor no coincide con lo que React renderiza en el cliente. Causas comunes:
- 1Valores que difieren entre server y client (fechas, Math.random(), window):
// Malo — Date.now() difiere entre server y client
<p>Generado: {Date.now()}</p>
// Bien — generar en servidor y pasar como prop, o suprimir en cliente- 2Extensiones del browser que modifican el DOM (password managers, adblockers)
- 3CSS-in-JS sin SSR support correcto
- 4Acceso a
localStorage/windowen el render inicial:
// Malo
const theme = localStorage.getItem("theme"); // falla en servidor
// Bien — solo en efectos o con "use client" + mounted state
const [theme, setTheme] = useState("");
useEffect(() => setTheme(localStorage.getItem("theme") ?? ""), []);Cómo diagnosticar: el error de React en consola muestra exactamente qué nodo difiere. suppressHydrationWarning en el elemento como último recurso (nunca para datos importantes).
40. ¿Qué cambió en Next.js 15 respecto a versiones anteriores?
Respuesta: Los cambios más importantes de Next.js 15:
- 1
fetchno cachea por defecto — el mayor breaking change. En v14,fetcheraforce-cachepor defecto. En v15 esno-store. Si migrás una app, los GETs que antes eran estáticos ahora son dinámicos.
- 2
paramsysearchParamsson ahora Promises:
// v15 — params es un Promise
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params; // hay que awaitearlo
}- 3
unstable_afterAPI — corre código después de que la response se envió al cliente (ideal para analytics, logging):
import { unstable_after as after } from "next/server";
export default function Page() {
after(() => { logPageView(); }); // corre después de enviar el HTML
return <PageContent />;
}- 4React 19 por defecto —
useActionState,useFormStatus,use(), optimistic updates mejorados.
- 5Turbopack estable para
dev— el bundler en Rust que reemplaza Webpack para desarrollo. Hasta 5x más rápido en cold start, hasta 96% más rápido en HMR.
Preguntas de Escenario (las que más se hacen en técnicas)
41. "Tenés una página de producto con datos que cambian una vez por día. ¿Cómo la implementarías?"
Respuesta modelo: ISR con revalidate: 86400 (24 horas). Se pre-genera en build time y se regenera en background una vez por día:
// App Router
export default async function ProductPage({ params }) {
const product = await fetch(`/api/products/${params.id}`, {
next: { revalidate: 86400 },
}).then(r => r.json());
return <Product data={product} />;
}Si los datos cambian de forma predecible (ej: el equipo editorial publica a las 9am), agregar un webhook desde el CMS que llame a revalidatePath o revalidateTag — así la página siempre está fresca cuando el contenido se actualiza.
42. "El dashboard de tu app carga lento. ¿Qué checkeás primero?"
Respuesta modelo estructurada:
- 1Network tab — ¿Cuánto tarda el TTFB? ¿Hay waterfall de API calls?
- 2¿Las queries son en paralelo?
Promise.allvs waterfall:
// Malo — waterfall
const user = await getUser(id);
const orders = await getOrders(id); // espera a user innecesariamente
// Bien — paralelo
const [user, orders] = await Promise.all([getUser(id), getOrders(id)]);- 3¿Hay N+1 queries? — un bucle que hace una query por item
- 4¿El bundle del cliente es grande? — Bundle Analyzer
- 5¿Las imágenes están optimizadas? —
next/image - 6¿Los Suspense boundaries están bien ubicados? — ¿el loading más lento bloquea el resto?
43. ¿Cómo pasarías un proyecto grande de Pages Router a App Router?
Respuesta: Migración incremental — App Router y Pages Router coexisten en el mismo proyecto:
- 1Empezar por rutas nuevas en
app/— no tocar nada depages/ - 2Migrar rutas simples (solo SSG, sin lógica compleja) primero
- 3El shared root layout (
app/layout.tsx) reemplaza a_app.tsxy_document.tsx - 4Migrar
getStaticProps→ Server Components confetch - 5Migrar
getServerSideProps→ Server Components concache: 'no-store' - 6Mover estado global a Context en un Client Component wrapper
- 7Migrar API routes a Route Handlers
- 8Al final, cuando todas las rutas estén en
app/, eliminar el directoriopages/
La trampa más común: intentar migrar todo de una vez. La migración incremental permite deployar en cada paso sin regressions.
Preguntas Bonus de Senior
44. ¿Cómo implementarías un sistema de feature flags en Next.js?
// lib/flags.ts
import { unstable_flag as flag } from "@vercel/flags/next";
export const newDashboard = flag({
key: "new-dashboard",
async decide() {
const user = await auth();
return user?.plan === "pro"; // flag por plan
},
});
// app/dashboard/page.tsx
import { newDashboard } from "@/lib/flags";
export default async function Dashboard() {
const showNew = await newDashboard();
return showNew ? <NewDashboard /> : <OldDashboard />;
}45. ¿Qué es el Turbopack y cómo impacta el desarrollo?
Respuesta: Turbopack es el nuevo bundler de Next.js escrito en Rust. En Next.js 15, es estable para next dev. Mejoras reportadas:
- ~76% más rápido en arranque del servidor de dev
- ~96% más rápido en Fast Refresh (HMR)
- Caché incremental persistente entre sesiones
Para habilitarlo: next dev --turbopack (o es el default en Next.js 15 para dev).
Todavía no se usa en next build (producción) — webpack sigue siendo el bundler de producción por ahora. Turbopack para prod está en roadmap.
Tips finales para la entrevista
Antes de terminar, tres patrones que distinguen a los candidatos que pasan:
1. Siempre mencionás los tradeoffs. No existe una respuesta universalmente correcta — SSR vs SSG depende de los requisitos. Decir "depende de X y Y" demuestra madurez.
2. Hablás de la experiencia del usuario final. LCP, CLS, TTFB no son solo métricas técnicas — explican por qué las decisiones de performance importan al negocio.
3. Conocés las versiones. Next.js 15 cambió defaults importantes (fetch caching, params como Promise). Mencionarlo muestra que seguís el ecosystem activamente.
*Este artículo es parte del material de preparación de [InterviewHack.ai](https://interviewhack.ai) — preparación de entrevistas con tu CV y las vacantes reales que te interesan.*