Cambiar tema

Guía completa de obtención de datos en Next.js Server Components: fetch, consultas a base de datos y mejores prácticas

Easton editorial illustration: route-map drafting table

La primera vez que escribí un componente en el App Router de Next.js, vi algo así:

async function Page() {
  const data = await fetch('...')
  return <div>{data}</div>
}

¿Eso es todo? ¿Un await directo? ¿Sin useEffect, sin useState y sin preocuparse por condiciones de carrera?

En ese momento me quedé un poco perdido. Acostumbrado al patrón del cliente en React, que de repente te digan que «un componente puede ser asíncrono» parecía que las reglas habían cambiado. Lo que más me inquietaba era: ¿debería usar la API fetch o consultar la base de datos directamente? Con fetch sientes que añades una capa extra; consultando la base de datos directamente, temes exponer las credenciales al cliente.

Si compartes estas dudas, este artículo es para ti. Repasamos la forma correcta de obtener datos en Next.js Server Components: cuándo usar fetch, cuándo consultar la base de datos, cómo escribir componentes async, cómo controlar la caché y los errores más frecuentes.

Fundamentos de obtención de datos en Server Components

¿Por qué los Server Components pueden usar await directamente?

Respuesta corta: los Server Components se ejecutan en el servidor, no en el navegador.

Suena obvio, pero la diferencia es clave. Los componentes React tradicionales se renderizan en el navegador y no pueden acceder directamente a bases de datos ni al sistema de archivos. Los Server Components se ejecutan en el servidor, así que pueden hacer cosas que antes solo tenían sentido en rutas API:

  • Conectarse directamente a la base de datos (Prisma, Drizzle, SQL nativo)
  • Leer el sistema de archivos (por ejemplo, archivos markdown)
  • Llamar servicios internos (sin preocuparse por CORS)
  • Acceder a variables de entorno y secretos (no se exponen al cliente)

Por eso este código es totalmente seguro:

// app/posts/page.tsx
import { db } from '@/lib/db'

async function PostsPage() {
  // Consulta directa: las credenciales no llegan al navegador
  const posts = await db.post.findMany()

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

export default PostsPage

Puntos clave:

  1. El componente se declara como async function
  2. Puedes usar await directamente en consultas a la base de datos
  3. No puedes usar hooks de React (useState, useEffect, etc.)
  4. Por defecto, el componente se renderiza en el servidor y el cliente solo recibe HTML

Tres formas principales de obtener datos

En Server Components tienes tres opciones:

1. API fetch

La forma más conocida, para llamar APIs externas o tus propios Route Handlers:

async function Page() {
  const res = await fetch('https://api.example.com/data')
  const data = await res.json()
  return <div>{data.title}</div>
}

2. Consulta directa a la base de datos

Con un ORM o cliente de base de datos:

import { db } from '@/lib/db'

async function Page() {
  const data = await db.posts.findFirst()
  return <div>{data.title}</div>
}

3. Server Actions

Para mutaciones (envío de formularios, eliminaciones, etc.): no solo obtienen datos, también los modifican:

async function createPost(formData: FormData) {
  'use server'
  const title = formData.get('title')
  await db.post.create({ data: { title } })
}

¿Cuál elegir? Sigue leyendo.

fetch vs consulta a base de datos: ¿cómo elegir?

Era mi mayor duda al empezar con App Router. Mirando atrás, la respuesta es bastante clara.

Árbol de decisión: decide en 5 segundos

Hazte tres preguntas:

  1. ¿Es un Server Component? → Si sí, continúa; si no (Client Component), salta a la 3
  2. ¿De dónde vienen los datos?
    • Tu propia base de datos → consulta directa
    • API de terceros → usa fetch
  3. ¿Necesitas datos desde un Client Component? → Crea una ruta API y usa fetch

Así de simple.

¿Por qué priorizar la consulta directa a la base de datos?

La recomendación oficial de Next.js es clara: en Server Components, no des la vuelta por una ruta API; consulta directamente.

Las razones son prácticas:

1. Ahorras un viaje HTTP

Mira la diferencia:

// ❌ Rodeo: Server Component → API Route → Database
async function Page() {
  const res = await fetch('/api/posts')  // Llamada HTTP
  const posts = await res.json()
  return <PostList posts={posts} />
}

// ✅ Directo: Server Component → Database
async function Page() {
  const posts = await db.post.findMany()  // Consulta directa
  return <PostList posts={posts} />
}

El segundo enfoque tiene una capa menos y responde más rápido. Puede parecer «¿qué diferencia hace?», pero se acumula: 100-200 ms menos en la carga de la página se nota.

2. Mejor seguridad de tipos

Con TypeScript + Prisma/Drizzle, consultar directamente te da inferencia de tipos completa:

// Tipos inferidos automáticamente, autocompletado perfecto
const post = await db.post.findFirst({
  include: { author: true, comments: true }
})

// post.author.name ← tiene sugerencias de tipo
// post.comments[0].content ← también

Con fetch tienes que definir tipos manualmente o usar as, lo que facilita errores.

3. Código más limpio

Sin archivos API extra, sin manejar códigos HTTP ni errores de red: la mitad de código.

¿Cuándo sí necesitas API/fetch?

No significa que nunca uses rutas API. Hay tres casos en los que sí:

Caso 1: un Client Component necesita datos

Los componentes cliente no pueden consultar la base de datos directamente (corren en el navegador), así que necesitas un endpoint API:

// app/api/posts/route.ts
export async function GET() {
  const posts = await db.post.findMany()
  return Response.json(posts)
}

// components/client-posts.tsx
'use client'
export function ClientPosts() {
  const [posts, setPosts] = useState([])

  useEffect(() => {
    fetch('/api/posts')
      .then(res => res.json())
      .then(setPosts)
  }, [])

  return <div>{/* render posts */}</div>
}

Caso 2: necesitas exponer una API externamente

Si tu app Next.js debe servir datos a otros servicios (app móvil, terceros), necesitas endpoints API públicos.

Caso 3: integración con servicios de terceros

Para GitHub API, OpenAI API, etc., usa fetch directamente:

async function Page() {
  const res = await fetch('https://api.github.com/users/vercel')
  const user = await res.json()
  return <div>Followers: {user.followers}</div>
}

Cómo escribir componentes async/await correctamente

Ya vimos qué elegir; ahora, cómo escribirlo.

Patrón básico: sorprendentemente simple

El componente async más básico se ve así:

async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.product.findUnique({
    where: { id: params.id }
  })

  if (!product) {
    return <div>Product not found</div>
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price}</p>
    </div>
  )
}

Sin estado de carga, sin useEffect: esperas los datos y renderizas. Limpio.

Paralelo vs secuencial: la diferencia de rendimiento es enorme

Aquí hay una trampa que yo mismo pisé.

Mira estos dos fragmentos: ¿cuál es más rápido?

// ❌ Secuencial: lento
async function Page() {
  const user = await db.user.findFirst()      // espera 200 ms
  const posts = await db.post.findMany()      // otros 150 ms
  const comments = await db.comment.findMany()  // otros 100 ms
  // total 450 ms
  return <Dashboard user={user} posts={posts} comments={comments} />
}

// ✅ Paralelo: rápido
async function Page() {
  const [user, posts, comments] = await Promise.all([
    db.user.findFirst(),       // se lanzan a la vez
    db.post.findMany(),        // se lanzan a la vez
    db.comment.findMany(),     // se lanzan a la vez
  ])
  // total 200 ms (el más lento)
  return <Dashboard user={user} posts={posts} comments={comments} />
}

¡Más del doble de diferencia! Si los datos no dependen unos de otros, usa siempre Promise.all.

Si hay dependencias, la cosa cambia:

// Debe ser secuencial: la segunda consulta depende de la primera
async function Page({ params }) {
  const user = await db.user.findUnique({ where: { id: params.id } })
  // primero el usuario, luego sus posts
  const posts = await db.post.findMany({ where: { authorId: user.id } })
  return <Profile user={user} posts={posts} />
}

Límites de Suspense: controlar la experiencia de carga

Puede que pienses: «Si los datos se obtienen en el servidor, ¿el usuario ve pantalla en blanco?»

Sí, pero Next.js ofrece loading.js y Suspense para mejorarlo.

Método 1: archivo loading.js

Crea loading.tsx en la carpeta de la ruta y funciona automáticamente:

// app/posts/loading.tsx
export default function Loading() {
  return <div>Loading posts...</div>
}

// app/posts/page.tsx
async function PostsPage() {
  const posts = await db.post.findMany()  // consulta lenta
  return <PostList posts={posts} />
}

El usuario verá primero «Loading posts…» y luego el contenido real.

Método 2: Suspense manual

Para un control más fino, envuelve con Suspense:

import { Suspense } from 'react'

async function SlowComponent() {
  const data = await slowQuery()  // 3 segundos
  return <div>{data}</div>
}

async function FastComponent() {
  const data = await fastQuery()  // 0,5 segundos
  return <div>{data}</div>
}

export default function Page() {
  return (
    <div>
      <FastComponent />  {/* lo rápido se muestra primero */}
      <Suspense fallback={<div>Loading...</div>}>
        <SlowComponent />  {/* lo lento espera sin bloquear lo de arriba */}
      </Suspense>
    </div>
  )
}

Error frecuente: Suspense mal colocado

Yo también lo hice:

// ❌ Error: Suspense dentro del componente async, no funciona
async function Page() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      {await slowQuery()}  {/* Suspense no lo intercepta */}
    </Suspense>
  )
}

// ✅ Correcto: Suspense envuelve el componente async por fuera
export default function Layout() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <SlowPage />  {/* componente async */}
    </Suspense>
  )
}

Suspense debe estar fuera del componente async para capturar la promesa.

Deduplicación automática de peticiones

Otra ventaja: la misma petición llamada varias veces en un render, Next.js la deduplica.

async function Header() {
  const user = await db.user.findFirst()  // consulta 1
  return <div>{user.name}</div>
}

async function Sidebar() {
  const user = await db.user.findFirst()  // consulta 2, pero no se ejecuta de verdad
  return <div>{user.name}</div>
}

export default function Page() {
  return (
    <div>
      <Header />
      <Sidebar />
      {/* en la práctica solo se consulta la base de datos una vez */}
    </div>
  )
}

Next.js recuerda el primer resultado; las llamadas siguientes devuelven la caché. Puedes usar la misma fuente de datos en varios componentes sin miedo al rendimiento.

Estrategias de caché y revalidación de datos

En caché, Next.js 15 trae un cambio grande; mucha gente ha caído en la trampa.

Next.js 15 cambió el valor por defecto de la caché

Antes (Next.js 14): fetch usaba por defecto cache: 'force-cache' y cacheaba siempre.

Ahora (Next.js 15): fetch usa por defecto cache: 'no-store', sin caché; cada vez vuelve a obtener.

¿Por qué? Oficialmente, porque muchos se confundían con la caché: creían que los datos se actualizaban en tiempo real y seguían viendo datos viejos. Ahora el default sin caché es más intuitivo.

¿Qué implica? Si migras de 14 a 15, puede que notes páginas más lentas: interfaces que antes estaban cacheadas ahora se llaman en cada petición.

Tres estrategias de caché

Elige según el tipo de dato:

1. Caché completa (sitios estáticos)

async function BlogPost({ slug }) {
  const post = await fetch(`https://api.example.com/posts/${slug}`, {
    cache: 'force-cache'  // caché permanente hasta reconstruir
  })
  return <article>{post.content}</article>
}

Ideal para: artículos de blog, fichas de producto, documentación — contenido que cambia poco.

2. Sin caché (datos en tiempo real)

async function StockPrice() {
  const price = await fetch('https://api.example.com/stock', {
    cache: 'no-store'  // siempre vuelve a obtener
  })
  return <div>Current price: {price}</div>
}

Ideal para: cotizaciones, comentarios en vivo, estado de usuario — debe estar al día.

3. Revalidación programada (ISR)

async function ProductList() {
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 }  // expira a los 60 s y se vuelve a obtener
  })
  return <div>{products.map(p => <Card key={p.id} {...p} />)}</div>
}

Ideal para: listados de productos, portada de noticias — toleran unos segundos de retraso, pero no pueden quedar muy desactualizados.

Revalidación manual: actualizar en cuanto cambian los datos

A veces modificas datos (por ejemplo, un usuario publica un post) y necesitas refrescar la caché al instante. Next.js ofrece dos APIs:

1. revalidatePath (refresca toda la página)

'use server'
import { revalidatePath } from 'next/cache'

async function createPost(formData: FormData) {
  await db.post.create({ data: {...} })
  revalidatePath('/posts')  // refresca la caché de /posts
}

2. revalidateTag (refresca etiquetas concretas)

Control más granular:

// Etiqueta al obtener datos
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts'] }  // etiqueta 'posts'
  })
  return res.json()
}

// Refresca esa etiqueta cuando haga falta
'use server'
import { revalidateTag } from 'next/cache'

async function createPost() {
  await db.post.create({ data: {...} })
  revalidateTag('posts')  // solo la caché con etiqueta 'posts'
}

Manejo de errores y optimización de rendimiento

Manejo de errores: no dejes que la página se caiga

Si falla la obtención de datos en Server Components, por defecto revienta toda la página. Hay que manejarlo bien.

Método 1: try/catch

async function Page() {
  try {
    const data = await fetch('https://api.example.com/data')
    if (!data.ok) throw new Error('Failed to fetch')
    return <div>{data.title}</div>
  } catch (error) {
    return <div>Something went wrong. Please try again.</div>
  }
}

Método 2: archivo error.js

Crea error.tsx en la carpeta de la ruta; captura errores de esa ruta y sus hijas:

// app/posts/error.tsx
'use client'  // el error boundary debe ser componente cliente

export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}

Trampa: redirect dentro de try/catch

// ❌ Error: el error de redirect lo atrapa catch
async function Page() {
  try {
    const user = await getUser()
    if (!user) redirect('/login')  // el error lanzado aquí lo atrapa catch
  } catch (error) {
    return <div>Error</div>  // ¡redirect no funciona!
  }
}

// ✅ Correcto: redirect fuera de try/catch
async function Page() {
  let user
  try {
    user = await getUser()
  } catch (error) {
    return <div>Error</div>
  }

  if (!user) redirect('/login')  // ahora redirige bien
}

Errores frecuentes y soluciones

Estos son los que yo mismo he pisado:

Error 1: fetch en servidor con ruta relativa

// ❌ Error: en servidor no hay base URL
async function Page() {
  const data = await fetch('/api/posts')  // ¡falla!
}

// ✅ Correcto: ruta absoluta
async function Page() {
  const data = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/posts`)
}

// ✅ Mejor: consulta directa, sin fetch
async function Page() {
  const posts = await db.post.findMany()
}

Error 2: olvidar comprobar response.ok

// ❌ Error: fetch no lanza error automáticamente
async function Page() {
  const res = await fetch('https://api.example.com/data')
  const data = await res.json()  // si es 404, aquí hay problemas
  return <div>{data.title}</div>
}

// ✅ Correcto: comprueba el estado
async function Page() {
  const res = await fetch('https://api.example.com/data')

  if (!res.ok) {
    throw new Error(`HTTP error! status: ${res.status}`)
  }

  const data = await res.json()
  return <div>{data.title}</div>
}

Error 3: llamar a un Route Handler desde un Server Component

// ❌ No recomendado: innecesario
async function Page() {
  const res = await fetch('/api/posts')  // ¿para qué dar la vuelta?
  const posts = await res.json()
  return <PostList posts={posts} />
}

// ✅ Recomendado: consulta directa
async function Page() {
  const posts = await db.post.findMany()
  return <PostList posts={posts} />
}

Caso práctico: construir una página de blog

Después de tanta teoría, un ejemplo completo.

Supongamos una página de detalle de artículo que necesita:

  • Mostrar el contenido del artículo
  • Mostrar información del autor
  • Mostrar artículos relacionados

Estructura de archivos

app/
  posts/
    [slug]/
      page.tsx       ← página de detalle
      loading.tsx    ← estado de carga
      error.tsx      ← manejo de errores

Código de implementación

// app/posts/[slug]/page.tsx
import { db } from '@/lib/prisma'
import { Suspense } from 'react'
import { notFound } from 'next/navigation'

// Componente principal
export default async function PostPage({
  params,
}: {
  params: { slug: string }
}) {
  // Artículo y autor en paralelo
  const [post, author] = await Promise.all([
    db.post.findUnique({
      where: { slug: params.slug },
    }),
    db.user.findFirst(),
  ])

  if (!post) {
    notFound()  // muestra 404
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <AuthorCard author={author} />
      <div>{post.content}</div>

      {/* Recomendaciones pueden cargar más tarde sin bloquear el contenido principal */}
      <Suspense fallback={<div>Loading recommendations...</div>}>
        <RecommendedPosts currentPostId={post.id} />
      </Suspense>
    </article>
  )
}

// Tarjeta de autor (render directo, datos ya disponibles)
function AuthorCard({ author }) {
  return (
    <div>
      <img src={author.avatar} alt={author.name} />
      <span>{author.name}</span>
    </div>
  )
}

// Artículos recomendados (componente async, carga independiente)
async function RecommendedPosts({ currentPostId }: { currentPostId: string }) {
  const recommended = await db.post.findMany({
    where: {
      id: { not: currentPostId },
      published: true,
    },
    take: 3,
  })

  return (
    <div>
      <h3>You might also like</h3>
      {recommended.map((post) => (
        <a key={post.id} href={`/posts/${post.slug}`}>
          {post.title}
        </a>
      ))}
    </div>
  )
}

// Caché: revalidar contenido cada hora
export const revalidate = 3600

// Parámetros estáticos (opcional, para generación estática)
export async function generateStaticParams() {
  const posts = await db.post.findMany({
    select: { slug: true },
  })

  return posts.map((post) => ({
    slug: post.slug,
  }))
}
// app/posts/[slug]/loading.tsx
export default function Loading() {
  return (
    <div>
      <div className="skeleton h-12 w-3/4" />
      <div className="skeleton h-4 w-1/4 mt-4" />
      <div className="skeleton h-64 mt-8" />
    </div>
  )
}
// app/posts/[slug]/error.tsx
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div>
      <h2>Failed to load post</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}

Explicación de decisiones clave

  1. ¿Por qué consulta directa? → Server Component; no hace falta pasar por ruta API
  2. ¿Por qué artículo y autor en paralelo? → Consultas independientes; en paralelo es más rápido
  3. ¿Por qué Suspense en recomendaciones? → No son críticas; pueden cargar tarde sin bloquear lo principal
  4. ¿Por qué revalidate: 3600? → El contenido cambia poco; una hora de caché alivia la base de datos

Conclusión

En resumen, los puntos clave son pocos:

  1. En Server Components, prioriza consulta directa a la base de datos, salvo que necesites datos desde Client Components o integrar APIs de terceros.
  2. Los componentes async/await son sencillos, pero recuerda Promise.all en paralelo y Suspense para la carga.
  3. Next.js 15 no cachea por defecto; elige force-cache, no-store o revalidate según el dato.
  4. Maneja bien los errores: comprueba response.ok, usa error.tsx y no metas redirect en try/catch.

Obtener datos en Server Components es mucho más simple que en el cliente. Sin estados de carga, condiciones de carrera ni cancelación de peticiones. Si dudabas en migrar a App Router, solo por esto ya merece la pena probarlo.

Úsalo con confianza en tu próximo proyecto; si te atascas, vuelve a este artículo.

Flujo completo de obtención de datos en Next.js Server Components

Pasos completos desde elegir fetch vs base de datos hasta patrones async, caché y manejo de errores

⏱️ Estimated time: 1 hr

  1. 1

    Step 1: Elegir fetch vs consulta a base de datos

    Consulta directa (recomendado):
    • Más rápido: menos una capa API, menor latencia
    • Más seguro: las credenciales de la base de datos no se exponen al cliente
    • Aplica cuando la base de datos es accesible desde el servidor

    Ejemplo:
    ```tsx
    import { db } from '@/lib/db'

    export default async function Page() {
    const users = await db.user.findMany()
    return <div>{users.map(u => <div key={u.id}>{u.name}</div>)}</div>
    }
    ```

    Usar fetch:
    • Aplica para APIs de terceros
    • Aplica cuando necesitas CORS
    • Nota: Next.js 15 no cachea por defecto

    Ejemplo:
    ```tsx
    export default async function Page() {
    const res = await fetch('https://api.example.com/data', {
    cache: 'force-cache' // en Next.js 15 hay que configurarlo explícitamente
    })
    const data = await res.json()
    return <div>{data}</div>
    }
    ```

    Recomendación: prioriza consulta directa; usa fetch solo para APIs de terceros
  2. 2

    Step 2: Patrón async/await en componentes

    Marca el componente como async:
    ```tsx
    export default async function Page() {
    const data = await fetchData()
    return <div>{data}</div>
    }
    ```

    Obtención en paralelo (Promise.all):
    ```tsx
    export default async function Page() {
    const [users, posts] = await Promise.all([
    fetchUsers(),
    fetchPosts()
    ])
    return <div>...</div>
    }
    ```

    Optimizar carga con Suspense:
    ```tsx
    import { Suspense } from 'react'

    export default function Page() {
    return (
    <Suspense fallback={<div>Loading...</div>}>
    <UserList />
    </Suspense>
    )
    }

    async function UserList() {
    const users = await fetchUsers()
    return <div>{users.map(...)}</div>
    }
    ```

    Puntos clave:
    • Los Server Components pueden ser async directamente
    • Usa Promise.all para peticiones en paralelo
    • Usa Suspense para mejorar la experiencia de carga
  3. 3

    Step 3: Configurar estrategia de caché

    Next.js 15 no cachea por defecto; hay que configurarlo explícitamente:

    Sin caché (datos en tiempo real):
    ```tsx
    fetch(url, { cache: 'no-store' })
    ```

    Caché permanente (datos estáticos):
    ```tsx
    fetch(url, { cache: 'force-cache' })
    ```

    Actualización programada (ISR):
    ```tsx
    fetch(url, { next: { revalidate: 3600 } })
    ```

    Recomendaciones:
    • Datos en tiempo real → cache: 'no-store'
    • Datos estáticos → cache: 'force-cache'
    • Actualizaciones frecuentes pero no en tiempo real → revalidate

    Nota: la filosofía de Next.js 15 es explícito mejor que implícito; piensa qué datos deben cachearse.
  4. 4

    Step 4: Manejo de errores

    Comprobar response.ok:
    ```tsx
    const res = await fetch(url)
    if (!res.ok) {
    throw new Error('Failed to fetch')
    }
    const data = await res.json()
    ```

    Usar error.tsx como red de seguridad:
    ```tsx
    // app/page/error.tsx
    'use client'
    export default function Error({ error, reset }) {
    return (
    <div>
    <h2>Error: {error.message}</h2>
    <button onClick={reset}>Reintentar</button>
    </div>
    )
    }
    ```

    Cuidado con redirect:
    ```tsx
    // ❌ Error: redirect dentro de try/catch
    try {
    if (!user) redirect('/login')
    } catch (e) {
    // redirect lanza un error que catch captura
    }

    // ✅ Correcto: redirect fuera de try/catch
    if (!user) redirect('/login')
    try {
    // otra lógica
    } catch (e) {
    // manejo de errores
    }
    ```

    Puntos clave:
    • Comprueba response.ok
    • Usa error.tsx como respaldo
    • No pongas redirect dentro de try/catch

FAQ

¿Por qué los Server Components pueden usar await directamente?
Porque los Server Components se ejecutan en el servidor, no en el navegador.

Pueden:
• Conectarse directamente a la base de datos (Prisma, Drizzle, SQL nativo)
• Leer el sistema de archivos (por ejemplo, archivos markdown)
• Llamar servicios internos (sin preocuparse por CORS)
• Acceder a variables de entorno y secretos (sin exponerlos al cliente)

Este código es totalmente seguro:
```tsx
import { db } from '@/lib/db'

export default async function Page() {
const users = await db.user.findMany() // las credenciales no se exponen
return <div>{users.map(...)}</div>
}
```

Ventajas:
• Sin useEffect ni useState
• Sin condiciones de carrera
• Sin gestionar estados de carga
• Obtener datos es mucho más simple que en el cliente
¿Cuándo usar fetch y cuándo consultar la base de datos directamente?
Consulta directa (recomendado):
• Más rápido: menos una capa API, menor latencia (servidor a BD suele ser <10 ms; cliente a servidor puede ser 100 ms+)
• Más seguro: las credenciales no llegan al cliente
• Aplica cuando la base de datos es accesible desde el servidor

Usar fetch:
• APIs de terceros
• Escenarios con CORS
• Cuando ya existe una API y no quieres cambiar la arquitectura

Recomendación:
• Prioriza consulta directa
• Usa fetch solo para APIs de terceros
• Evita fetch a tu propia API desde un Server Component (es redundante)

Comparación:
```tsx
// ❌ Antipatrón: fetch a tu propia API desde Server Component
const res = await fetch('/api/users')
const users = await res.json()

// ✅ Correcto: consulta directa
const users = await db.user.findMany()
```
¿Cómo escribir componentes async?
Marca el componente como async:
```tsx
export default async function Page() {
const data = await fetchData()
return <div>{data}</div>
}
```

Obtención en paralelo (Promise.all):
```tsx
export default async function Page() {
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
])
return <div>...</div>
}
```

Optimizar con Suspense:
```tsx
import { Suspense } from 'react'

export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserList />
</Suspense>
)
}

async function UserList() {
const users = await fetchUsers()
return <div>{users.map(...)}</div>
}
```

Puntos clave:
• Los Server Components pueden ser async
• Usa Promise.all en paralelo
• Usa Suspense para la experiencia de carga
¿Cómo configurar la caché en Next.js 15?
Next.js 15 no cachea por defecto; hay que configurarlo explícitamente:

Sin caché (datos en tiempo real):
```tsx
fetch(url, { cache: 'no-store' })
```

Caché permanente (datos estáticos):
```tsx
fetch(url, { cache: 'force-cache' })
```

Actualización programada (ISR):
```tsx
fetch(url, { next: { revalidate: 3600 } })
```

Recomendaciones:
• Datos en tiempo real → cache: 'no-store'
• Datos estáticos → cache: 'force-cache'
• Actualizaciones frecuentes pero no en tiempo real → revalidate

Nota: la filosofía de Next.js 15 es explícito mejor que implícito. Es un cambio breaking; tenlo en cuenta al migrar.
¿Cómo manejar errores en Server Components?
Comprobar response.ok:
```tsx
const res = await fetch(url)
if (!res.ok) {
throw new Error('Failed to fetch')
}
const data = await res.json()
```

Usar error.tsx como respaldo:
```tsx
// app/page/error.tsx
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>Error: {error.message}</h2>
<button onClick={reset}>Reintentar</button>
</div>
)
}
```

Cuidado con redirect:
```tsx
// ❌ Error: redirect dentro de try/catch
try {
if (!user) redirect('/login')
} catch (e) {
// redirect lanza un error que catch captura
}

// ✅ Correcto: redirect fuera de try/catch
if (!user) redirect('/login')
try {
// otra lógica
} catch (e) {
// manejo de errores
}
```

Puntos clave:
• Comprueba response.ok
• Usa error.tsx como respaldo
• No pongas redirect en try/catch (redirect lanza un error)

14 min de lectura · Publicado el: 19 dic 2025 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog