React + TypeScript
Semana 7 de 9·8h

Capa de servicios y conexión a la base de datos

Capa de servicios con Fetch tipado, introducción a Prisma ORM 7 y primera conexión real a PostgreSQL.

Fetch APITypeScriptPrismaPostgreSQL
Objetivos de aprendizaje
  • Consolidar el patrón fetchJSON genérico como capa de servicios del frontend
  • Comprender por qué los componentes nunca llaman a fetch directamente
  • Instalar y configurar Prisma ORM 7 con @prisma/adapter-pg
  • Definir el schema.prisma con los modelos Articulo y Categoria
  • Reemplazar el array mock del backend por consultas reales a PostgreSQL con Prisma

🎯 Objetivo de la semana: Al terminar habrás consolidado articulosService.ts como la capa de servicios tipada del frontend y conectado el backend a PostgreSQL real usando Prisma ORM 7 — sin cambiar una sola línea del código React.

🔑 Concepto clave: Separación cliente-servidor — el frontend no sabe si los datos vienen de un array en memoria, de PostgreSQL o de una API externa. Solo ve el contrato de articulosService. Si el backend cambia, solo cambia el backend.

🛠 Tarea práctica: Esta semana tiene dos mitades:

  • Frontend: revisar y consolidar articulosService.ts de S6 (manejo de errores correcto, tipos genéricos, resolverImagen)
  • Backend: instalar Prisma 7, definir schema.prisma, ejecutar la primera migración y reemplazar el array mock por prisma.articulo.findMany()

📋 Entregable: GET /api/v1/articulos devuelve artículos reales de PostgreSQL. La HomePage los muestra sin cambiar una sola línea del código React. TanStack Query y Axios no se usan en este proyecto.


1. La capa de servicios: por qué nunca fetch directo

En la Semana 6 creaste articulosService.ts. Revisemos por qué el patrón importa y qué lo hace correcto.

La regla es simple: ningún componente llama a fetch directamente. Si HomePage hace fetch('/api/v1/articulos') y la URL cambia a /api/v2/articulos, tienes que encontrar y cambiar cada componente que haga esa llamada. Con la capa de servicios, el cambio es en un solo lugar.

El fetchJSON<T> genérico es el corazón del servicio. Hay dos detalles críticos que el código debe tener:

typescript
// src/services/articulosService.ts — patrón correcto completo
async function fetchJSON<T>(url: string, init?: RequestInit, noCache = false): Promise<T> {
  const res = await fetch(`${BASE}${url}`, {
    headers: { 'Content-Type': 'application/json' },
    cache: noCache ? 'no-store' : 'default',
    ...init,
  })

  // ⚠️ fetch NO lanza error en 4xx/5xx — hay que verificar res.ok manualmente
  if (!res.ok) {
    const body = await res.json().catch(() => ({}))
    // Usa el mensaje del servidor si existe, no solo el código HTTP
    throw new Error(body.message ?? `HTTP ${res.status}`)
  }

  return res.json() as Promise<T>
}

El flag noCache es importante para el panel admin: el backend usa Cache-Control: max-age=60 en los GET, lo que hace que el navegador sirva respuestas cacheadas en lugar de pedir datos frescos. Al llamar con noCache: true, el servicio fuerza una petición real al servidor:

typescript
// Panel admin — siempre datos frescos
const res = await articulosService.getAll(undefined, true) // noCache = true

// Página pública — puede usar caché
const res = await articulosService.getAll({ estado: 'publicado' })

2. Prisma ORM 7: introducción

Prisma es un ORM (Object-Relational Mapper) que genera un cliente TypeScript fuertemente tipado a partir de tu schema. En lugar de escribir SQL manualmente, escribes consultas como objetos TypeScript y Prisma genera el SQL correcto para tu base de datos.

Prisma 7 tiene cambios breaking respecto a versiones anteriores:

AspectoPrisma < 7Prisma 7
DATABASE_URLEn schema.prismaEn prisma.config.ts
DriverInterno@prisma/adapter-pg explícito
Cliente generadonode_modules/.prisma/generated/prisma/ (configurable)
Generatorprisma-client-jsprisma-client

La instalación en el backend:

bash
# Desde backend/
pnpm add prisma @prisma/client @prisma/adapter-pg
pnpm approve-builds  # autoriza la compilación de binarios nativos de Prisma

3. Configurar Prisma 7 en el backend

prisma.config.ts

En Prisma 7 la DATABASE_URL ya no va en schema.prisma — va en un archivo de configuración dedicado:

typescript
// backend/prisma.config.ts
import path from 'node:path'
import { defineConfig } from 'prisma/config'

export default defineConfig({
  earlyAccess: true,
  schema: path.join(import.meta.dirname, 'prisma/schema.prisma'),
})

La DATABASE_URL se lee del .env directamente a través del adapter PrismaPg.

schema.prisma

prisma
// backend/prisma/schema.prisma
generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}

model Categoria {
  id        Int        @id @default(autoincrement())
  nombre    String
  slug      String     @unique
  color     String
  articulos Articulo[]
}

model Articulo {
  id               Int       @id @default(autoincrement())
  titulo           String
  slug             String    @unique
  extracto         String?
  contenido        String?
  imagen           String?
  categoriaId      Int
  categoria        Categoria @relation(fields: [categoriaId], references: [id])
  estado           EstadoArticulo @default(borrador)
  autor            String         @default("Administrador")
  tiempoLectura    Int            @default(5)
  fechaPublicacion DateTime       @default(now())
  createdAt        DateTime       @default(now())
  updatedAt        DateTime       @updatedAt
}

enum EstadoArticulo {
  borrador
  publicado
  archivado
}

db.ts — cliente con adapter

typescript
// backend/src/config/db.ts
import { PrismaPg } from '@prisma/adapter-pg'
import { PrismaClient } from '../generated/prisma/client'
import 'dotenv/config'

const adapter = new PrismaPg({
  connectionString: process.env.DATABASE_URL ?? ''
})

export const prisma = new PrismaClient({ adapter })

prisma.$connect()
  .then(() => console.log('✅ PostgreSQL conectado via Prisma'))
  .catch((e) => console.error('❌ Error Prisma:', e))

4. Primera migración y reemplazar el mock

Ejecutar la migración

bash
# Desde backend/
npx prisma migrate dev --name init
npx prisma generate  # regenerar el cliente TypeScript

Esto crea las tablas en PostgreSQL y genera el cliente tipado en src/generated/prisma/.

Seed de datos iniciales

bash
# backend/package.json
{
  "scripts": {
    "seed": "npx tsx prisma/seed.ts"
  }
}
typescript
// backend/prisma/seed.ts
import { prisma } from '../src/config/db'

async function main() {
  // Crear categorías
  const tech = await prisma.categoria.upsert({
    where: { slug: 'tecnologia' },
    update: {},
    create: { nombre: 'Tecnología', slug: 'tecnologia', color: '#3b82f6' },
  })

  // Crear artículo de prueba
  await prisma.articulo.upsert({
    where: { slug: 'primer-articulo' },
    update: {},
    create: {
      titulo: 'Mi primer artículo',
      slug: 'primer-articulo',
      extracto: 'Este es el extracto del artículo.',
      contenido: '<p>Contenido del artículo.</p>',
      categoriaId: tech.id,
      estado: 'publicado',
    },
  })

  console.log('✅ Seed completado')
}

main().catch(console.error).finally(() => prisma.$disconnect())

Reemplazar el mock en el router

typescript
// backend/src/routes/articulos.ts — conectado a Prisma
import { Router } from 'express'
import { prisma } from '../config/db'

const router = Router()

router.get('/', async (req, res) => {
  const { estado, busqueda } = req.query
  const where: Record<string, unknown> = {}

  if (estado && estado !== 'todos') where.estado = String(estado)
  if (busqueda) where.titulo = { contains: String(busqueda), mode: 'insensitive' }

  const articulos = await prisma.articulo.findMany({
    where,
    include: { categoria: true },
    orderBy: { createdAt: 'desc' },
  })

  res.json({ status: 'success', data: articulos })
})

router.get('/:slug', async (req, res) => {
  const articulo = await prisma.articulo.findUnique({
    where: { slug: req.params.slug },
    include: { categoria: true },
  })
  if (!articulo) return res.status(404).json({ status: 'error', message: 'No encontrado' })
  res.json({ status: 'success', data: articulo })
})

export default router

Resultado esperado: GET /api/v1/articulos devuelve artículos de PostgreSQL. La HomePage y DetallePage los muestran sin cambiar una sola línea del código React — el contrato de articulosService no cambió.


Actividades prácticas

Actividad 1 — Revisar articulosService (30 min) Verifica que tu articulosService.ts verifica res.ok, lanza Error con el mensaje del servidor y tiene resolverImagen. Llama a articulosService.getAll({ estado: 'publicado' }) desde la HomePage y confirma que los artículos se cargan desde el servidor Express.

Actividad 2 — Configurar Prisma 7 (60 min) Instalar Prisma, crear schema.prisma con los modelos exactos del proyecto, ejecutar migrate dev --name init, verificar que las tablas existen en PostgreSQL con npx prisma studio.

Actividad 3 — Seed y primer GET con Prisma (45 min) Crear prisma/seed.ts con al menos 2 categorías y 4 artículos. Ejecutar pnpm seed. Reemplazar el array mock en el router por prisma.articulo.findMany(). Verificar que GET /api/v1/articulos devuelve los datos del seed.

Actividad 4 — Verificar end-to-end (30 min) Con el backend conectado a PostgreSQL y el frontend corriendo, confirma que la HomePage muestra los artículos del seed. Añade un artículo directamente en Prisma Studio y verifica que aparece en el frontend al recargar.


🛠 Proyecto CMS — Semana 7: Backend conectado a PostgreSQL

Al terminar esta semana:

  1. El backend usa Prisma 7 para leer artículos de PostgreSQL
  2. El frontend usa articulosService (no TanStack Query, no Axios) para consumir la API
  3. npm run dev desde la raíz levanta ambos procesos — frontend en 5173, backend en 3001
  4. npx prisma studio desde backend/ permite ver y editar los datos directamente

📁 Archivos de esta semana

code
backend/
├── prisma.config.ts              ← NUEVO: configuración Prisma 7 (apunta al schema)
├── prisma/                       ← carpeta NUEVA esta semana
│   ├── schema.prisma             ← NUEVO: modelos Articulo y Categoria
│   ├── seed.ts                   ← NUEVO: datos iniciales de prueba
│   └── migrations/               ← GENERADO automáticamente por `npx prisma migrate dev`
│       └── 20xxxxxx_init/
└── src/
    ├── config/                   ← carpeta NUEVA esta semana
    │   └── db.ts                 ← NUEVO: cliente Prisma con PrismaPg adapter
    ├── generated/                ← GENERADO por `npx prisma generate`
    │   └── prisma/               ← cliente TypeScript tipado, NO editar a mano
    └── routes/
        └── articulos.ts          ← ACTUALIZADO: mock → prisma.articulo.findMany()

Prisma 7 — dónde van los archivos:

  • DATABASE_URL → en backend/.env, NO en schema.prisma
  • prisma.config.ts → en la raíz de backend/, no dentro de src/
  • Cliente generado → en src/generated/prisma/ (no en node_modules/)
  • Import del cliente → import { PrismaClient } from '../generated/prisma/client'

En la Semana 8 añadirás los endpoints de escritura (POST, PUT, DELETE) y la arquitectura MVC completa con controllers, services y la capa de queries Prisma.


📥 Descarga el entregable de esta semana

¿No lograste completar la semana o quieres comparar tu código con la solución? El entregable de la Semana 7 está incluido junto con el de la Semana 8 (se implementaron en el mismo sprint):

⬇️ Descargar entregable Semanas 7-8 (.zip)

Qué incluye: backend completo con Prisma 7 (prisma.config.ts, schema.prisma, src/config/db.ts, src/generated/prisma/), primera migración y seed de datos, rutas GET conectadas a PostgreSQL y la arquitectura MVC completa con todos los endpoints CRUD.

Setup después de descomprimir:

bash
cd blog-cms
npm install
cd backend && pnpm install
# Crea backend/.env con: DATABASE_URL="postgresql://..."
npx prisma migrate dev
pnpm seed
cd ..
npm run dev