Obtención de Datos

Ahora que has creado y poblado tu base de datos, hablemos sobre las diferentes formas en que puedes obtener datos para tu aplicación y construir tu página de resumen del panel de control.

Elegir cómo obtener datos

Capa de API

Las APIs son una capa intermedia entre el código de tu aplicación y la base de datos. Hay algunos casos en los que podrías usar una API:

  • Si estás utilizando servicios de terceros que proporcionan una API.
  • Si estás obteniendo datos desde el cliente, quieres tener una capa de API que se ejecute en el servidor para evitar exponer los secretos de tu base de datos al cliente.

En Next.js, puedes crear endpoints de API usando Manejadores de Ruta.

Consultas a la base de datos

Cuando creas una aplicación full-stack, también necesitarás escribir lógica para interactuar con tu base de datos. Para bases de datos relacionales como Postgres, puedes hacer esto con SQL o con un ORM.

Hay algunos casos en los que tienes que escribir consultas a la base de datos:

  • Al crear tus endpoints de API, necesitas escribir lógica para interactuar con tu base de datos.
  • Si estás usando Componentes del Servidor de React (obteniendo datos en el servidor), puedes omitir la capa de API y consultar tu base de datos directamente sin arriesgarte a exponer los secretos de tu base de datos al cliente.

Aprendamos más sobre los Componentes del Servidor de React.

Usar Componentes del Servidor para obtener datos

Por defecto, las aplicaciones de Next.js usan Componentes del Servidor de React. Obtener datos con Componentes del Servidor es un enfoque relativamente nuevo y hay algunos beneficios al usarlos:

  • Los Componentes del Servidor soportan Promesas de JavaScript, proporcionando una solución nativa para tareas asíncronas como la obtención de datos. Puedes usar la sintaxis async/await sin necesidad de useEffect, useState u otras bibliotecas para obtener datos.
  • Los Componentes del Servidor se ejecutan en el servidor, por lo que puedes mantener las obtenciones de datos costosas y la lógica en el servidor, enviando solo el resultado al cliente.
  • Dado que los Componentes del Servidor se ejecutan en el servidor, puedes consultar la base de datos directamente sin una capa de API adicional. Esto te ahorra escribir y mantener código adicional.

Usar SQL

Para tu aplicación de panel de control, escribirás consultas a la base de datos usando la biblioteca postgres.js y SQL. Hay algunas razones por las que usaremos SQL:

  • SQL es el estándar de la industria para consultar bases de datos relacionales (por ejemplo, los ORMs generan SQL internamente).
  • Tener un conocimiento básico de SQL puede ayudarte a entender los fundamentos de las bases de datos relacionales, permitiéndote aplicar tu conocimiento a otras herramientas.
  • SQL es versátil, permitiéndote obtener y manipular datos específicos.
  • La biblioteca postgres.js proporciona protección contra inyecciones SQL.

No te preocupes si no has usado SQL antes - hemos proporcionado las consultas por ti.

Ve a /app/lib/data.ts. Aquí verás que estamos usando postgres. La función sql te permite consultar tu base de datos:

/app/lib/data.ts
import postgres from 'postgres';
 
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
 
// ...

Puedes llamar a sql en cualquier lugar del servidor, como un Componente del Servidor. Pero para permitirte navegar los componentes más fácilmente, hemos mantenido todas las consultas de datos en el archivo data.ts, y puedes importarlas en los componentes.

Nota: Si usaste tu propio proveedor de base de datos en el Capítulo 6, necesitarás actualizar las consultas de la base de datos para que funcionen con tu proveedor. Puedes encontrar las consultas en /app/lib/data.ts.

Obtener datos para la página de resumen del panel de control

Ahora que entiendes las diferentes formas de obtener datos, obtengamos datos para la página de resumen del panel de control. Navega a /app/dashboard/page.tsx, pega el siguiente código y tómate un tiempo para explorarlo:

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
 
export default async function Page() {
  return (
    <main>
      <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
        Panel de Control
      </h1>
      <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
        {/* <Card title="Recaudado" value={totalPaidInvoices} type="collected" /> */}
        {/* <Card title="Pendiente" value={totalPendingInvoices} type="pending" /> */}
        {/* <Card title="Total de Facturas" value={numberOfInvoices} type="invoices" /> */}
        {/* <Card
          title="Total de Clientes"
          value={numberOfCustomers}
          type="customers"
        /> */}
      </div>
      <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
        {/* <RevenueChart revenue={revenue}  /> */}
        {/* <LatestInvoices latestInvoices={latestInvoices} /> */}
      </div>
    </main>
  );
}

El código anterior está intencionalmente comentado. Ahora comenzaremos a examinar cada parte.

  • La page es un componente del servidor async. Esto te permite usar await para obtener datos.
  • También hay 3 componentes que reciben datos: <Card>, <RevenueChart>, y <LatestInvoices>. Actualmente están comentados y aún no implementados.

Obtener datos para <RevenueChart/>

Para obtener datos para el componente <RevenueChart/>, importa la función fetchRevenue de data.ts y llámala dentro de tu componente:

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  // ...
}

A continuación, hagamos lo siguiente:

  1. Descomenta el componente <RevenueChart/>.
  2. Navega al archivo del componente (/app/ui/dashboard/revenue-chart.tsx) y descomenta el código dentro de él.
  3. Revisa localhost:3000 y deberías ver un gráfico que usa los datos de revenue.
Gráfico de ingresos mostrando el total de ingresos de los últimos 12 meses

Continuemos importando más datos y mostrándolos en el panel de control.

Obtener datos para <LatestInvoices/>

Para el componente <LatestInvoices />, necesitamos obtener las últimas 5 facturas, ordenadas por fecha.

Podrías obtener todas las facturas y ordenarlas usando JavaScript. Esto no es un problema ya que nuestros datos son pequeños, pero a medida que tu aplicación crece, puede aumentar significativamente la cantidad de datos transferidos en cada solicitud y el JavaScript necesario para ordenarlos.

En lugar de ordenar las últimas facturas en memoria, puedes usar una consulta SQL para obtener solo las últimas 5 facturas. Por ejemplo, esta es la consulta SQL de tu archivo data.ts:

/app/lib/data.ts
// Obtener las últimas 5 facturas, ordenadas por fecha
const data = await sql<LatestInvoiceRaw[]>`
  SELECT invoices.amount, customers.name, customers.image_url, customers.email
  FROM invoices
  JOIN customers ON invoices.customer_id = customers.id
  ORDER BY invoices.date DESC
  LIMIT 5`;

En tu página, importa la función fetchLatestInvoices:

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue, fetchLatestInvoices } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  // ...
}

Luego, descomenta el componente <LatestInvoices />. También necesitarás descomentar el código relevante en el componente <LatestInvoices /> mismo, ubicado en /app/ui/dashboard/latest-invoices.

Si visitas tu localhost, deberías ver que solo se devuelven las últimas 5 desde la base de datos. ¡Esperamos que estés comenzando a ver las ventajas de consultar tu base de datos directamente!

Componente de últimas facturas junto al gráfico de ingresos

Práctica: Obtener datos para los componentes <Card>

Ahora es tu turno de obtener datos para los componentes <Card>. Las tarjetas mostrarán los siguientes datos:

  • Cantidad total de facturas recaudadas.
  • Cantidad total de facturas pendientes.
  • Número total de facturas.
  • Número total de clientes.

Nuevamente, podrías sentirte tentado a obtener todas las facturas y clientes, y usar JavaScript para manipular los datos. Por ejemplo, podrías usar Array.length para obtener el número total de facturas y clientes:

const totalInvoices = allInvoices.length;
const totalCustomers = allCustomers.length;

Pero con SQL, puedes obtener solo los datos que necesitas. Es un poco más largo que usar Array.length, pero significa que se necesita transferir menos datos durante la solicitud. Esta es la alternativa en SQL:

/app/lib/data.ts
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;

La función que necesitarás importar se llama fetchCardData. Necesitarás desestructurar los valores devueltos por la función.

Pista:

  • Revisa los componentes de tarjeta para ver qué datos necesitan.
  • Revisa el archivo data.ts para ver qué devuelve la función.

Una vez que estés listo, expande el siguiente toggle para ver el código final:

¡Genial! Ahora has obtenido todos los datos para la página de resumen del panel de control. Tu página debería verse así:

Página de panel de control con todos los datos obtenidos

Sin embargo... hay dos cosas que debes tener en cuenta:

  1. Las solicitudes de datos están bloqueándose involuntariamente entre sí, creando una cascada de solicitudes.
  2. Por defecto, Next.js prerrenderiza rutas para mejorar el rendimiento, esto se llama Renderizado Estático. Así que si tus datos cambian, no se reflejarán en tu panel de control.

Hablemos del punto 1 en este capítulo, luego veamos en detalle el punto 2 en el próximo capítulo.

¿Qué son las cascadas de solicitudes?

Una "cascada" se refiere a una secuencia de solicitudes de red que dependen de la finalización de solicitudes anteriores. En el caso de la obtención de datos, cada solicitud solo puede comenzar una vez que la solicitud anterior ha devuelto datos.

Diagrama mostrando tiempo con obtención de datos secuencial y paralela

Por ejemplo, necesitamos esperar a que fetchRevenue() se ejecute antes de que fetchLatestInvoices() pueda comenzar a ejecutarse, y así sucesivamente.

/app/dashboard/page.tsx
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // espera a que fetchRevenue() termine
const {
  numberOfInvoices,
  numberOfCustomers,
  totalPaidInvoices,
  totalPendingInvoices,
} = await fetchCardData(); // espera a que fetchLatestInvoices() termine

Este patrón no es necesariamente malo. Puede haber casos en los que quieras cascadas porque deseas que se cumpla una condición antes de hacer la siguiente solicitud. Por ejemplo, podrías querer obtener primero el ID y la información del perfil de un usuario. Una vez que tengas el ID, podrías proceder a obtener su lista de amigos. En este caso, cada solicitud depende de los datos devueltos por la solicitud anterior.

Sin embargo, este comportamiento también puede ser involuntario y afectar el rendimiento.

Obtención de datos en paralelo

Una forma común de evitar cascadas es iniciar todas las solicitudes de datos al mismo tiempo - en paralelo.

En JavaScript, puedes usar las funciones Promise.all() o Promise.allSettled() para iniciar todas las promesas al mismo tiempo. Por ejemplo, en data.ts, estamos usando Promise.all() en la función fetchCardData():

/app/lib/data.ts
export async function fetchCardData() {
  try {
    const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
    const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
    const invoiceStatusPromise = sql`SELECT
         SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
         SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
         FROM invoices`;
 
    const data = await Promise.all([
      invoiceCountPromise,
      customerCountPromise,
      invoiceStatusPromise,
    ]);
    // ...
  }
}

Al usar este patrón, puedes:

  • Comenzar a ejecutar todas las obtenciones de datos al mismo tiempo, lo cual es más rápido que esperar a que cada solicitud se complete en una cascada.
  • Usar un patrón nativo de JavaScript que puede aplicarse a cualquier biblioteca o framework.

Sin embargo, hay una desventaja de depender solo de este patrón de JavaScript: ¿qué pasa si una solicitud de datos es más lenta que todas las demás? Aprendamos más en el próximo capítulo.