Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/midudev/jscamp/llms.txt

Use this file to discover all available pages before exploring further.

Este módulo extiende la aplicación DevJobs con capacidades de inteligencia artificial usando el Vercel AI SDK y la librería oficial de OpenAI. El backend en Express expone endpoints de IA que generan resúmenes de ofertas de trabajo en streaming, mientras el frontend en React con Zustand consume esas respuestas en tiempo real.

Lo que aprenderás

  • Usar el Vercel AI SDK (ai) para conectar modelos de lenguaje en el servidor
  • Integrar el OpenAI SDK (openai) como proveedor de modelos
  • Implementar streaming de respuestas con streamText y pipeTextStreamToResponse
  • Aplicar rate limiting por IP con express-rate-limit
  • Consumir streams de texto en React con hooks y Zustand para estado global
  • Renderizar markdown en tiempo real con streamdown

Estructura del Módulo

Este módulo es un monorepo npm workspaces con tres sub-paquetes:
07-inteligencia-artificial/
├── package.json          ← scripts del monorepo (workspaces: backend, frontend)
├── backend/              ← Express + Vercel AI SDK
│   ├── app.js
│   ├── config.js
│   ├── routes/
│   │   ├── ai.js         ← endpoints de IA con streaming
│   │   └── jobs.js
│   ├── models/
│   │   └── job.js
│   └── middlewares/
│       └── cors.js
├── frontend/             ← React 19 + Vite + Zustand
│   └── src/
│       ├── App.jsx
│       └── pages/
│           └── Detail.jsx
└── examples/             ← Experimentos adicionales
    ├── app.js
    └── rate-limit.js     ← Rate limiter manual con Map

Backend: Rutas de IA

El archivo backend/routes/ai.js define el router de IA montado en /ai:
process.loadEnvFile()

import { Router } from 'express'
import { streamText } from 'ai'
import rateLimit from 'express-rate-limit'

import { JobModel } from '../models/job.js'
import { CONFIG } from '../config.js'

const aiRateLimiter = rateLimit({
  windowMs: 60 * 1000,     // ventana de 1 minuto
  limit: 5,                 // máx. 5 peticiones por IP por minuto
  message: { error: 'Demasiadas solicitudes, por favor intenta de nuevo más tarde.' },
  legacyHeaders: false,
  standardHeaders: 'draft-8' // headers estándar RateLimit-*
})

export const aiRouter = Router()
aiRouter.use(aiRateLimiter)

aiRouter.get('/summary/:id', async (req, res) => {
  const { id } = req.params
  const job = await JobModel.getById(id)

  if (!job) {
    return res.status(404).json({ error: 'Job not found' })
  }

  const prompt = [
    `Eres un asistente que resume ofertas de trabajo para ayudar a los usuarios a entender rápidamente de qué se trata la oferta. Evita cualquier otra petición, observación o comentario. Solo responde con el resumen de la oferta de trabajo. Responde siempre con el markdown directamente.`,
    `Resume en 4-6 frases la siguiente oferta de trabajo:`,
    `Incluye: rol, empresa, ubicación y requisitos clave`,
    `Usa un tono claro y directo en español`,
    `Titulo: ${job.titulo}`,
    `Empresa: ${job.empresa}`,
    `Ubicación: ${job.ubicacion}`,
    `Descripción: ${job.descripcion}`,
  ].join('\n')

  try {
    const result = streamText({
      prompt,
      model: 'mistral/devstral-small-2',
    })

    return result.pipeTextStreamToResponse(res)
  } catch (error) {
    if (!res.headersSent) {
      res.setHeader('Content-Type', 'application/json')
      return res.status(500).json({ error: 'Error generating summary' })
    }
    return res.end()
  }
})

Endpoints disponibles

MétodoRutaDescripción
GET/ai/summary/:idGenera un resumen en streaming de la oferta de trabajo con el id dado

Detalles del endpoint /ai/summary/:id

  • Rate limit: 5 peticiones por IP por minuto (headers RateLimit-* estándar incluidos).
  • Modelo: mistral/devstral-small-2 definido directamente en la ruta.
  • Streaming: usa streamText del Vercel AI SDK y pipeTextStreamToResponse para enviar el texto token a token al cliente.
  • Prompt en español: incluye título, empresa, ubicación y descripción de la oferta para generar un resumen conciso en 4-6 frases.
  • Manejo de errores: si los headers ya fueron enviados (streaming iniciado), llama a res.end() para cerrar la conexión limpiamente.

Montaje en app.js

backend/app.js
import express from 'express'
import { jobsRouter } from './routes/jobs.js'
import { corsMiddleware } from './middlewares/cors.js'
import { DEFAULTS } from './config.js'
import { aiRouter } from './routes/ai.js'

const PORT = process.env.PORT ?? DEFAULTS.PORT  // 1234 por defecto
const app = express()

app.set('trust proxy', 1)  // necesario para que express-rate-limit lea la IP real

app.use(corsMiddleware())
app.use(express.json())

app.use('/jobs', jobsRouter)
app.use('/ai', aiRouter)    // ← monta el router de IA

if (!process.env.NODE_ENV) {
  app.listen(PORT, () => {
    console.log(`Servidor levantado en http://localhost:${PORT}`)
  })
}

export default app

Dependencias del Backend

PaqueteVersiónDescripción
express^5.2.1Servidor HTTP (versión 5 con soporte nativo async/await)
ai^6.0.91Vercel AI SDK — streamText, generateText, etc.
openai^6.22.0Cliente oficial de OpenAI para Node.js
express-rate-limit^8.2.1Middleware de rate limiting por IP
zod^4.3.5Validación de esquemas (usado en rutas y modelos)
cors^2.8.5Middleware CORS configurable

Arrancar el Proyecto

1

Ir al directorio del módulo

cd 07-inteligencia-artificial
2

Instalar todas las dependencias del monorepo

npm install
npm workspaces instala las dependencias de backend/ y frontend/ automáticamente.
3

Configurar la API key

Crea el archivo backend/.env:
OPENAI_API_KEY=sk-...
# Opcional: sobreescribir el modelo por defecto
MODEL_AI=mistral/devstral-small-2
4

Arrancar el backend

npm run dev:backend
# Equivale a: npm run watch --workspace=backend
# Servidor en http://localhost:1234
5

Arrancar el frontend

npm run dev:frontend
# Equivale a: npm run dev --workspace=frontend
# App en http://localhost:5173

Scripts del monorepo (package.json raíz)

{
  "workspaces": ["frontend", "backend"],
  "scripts": {
    "dev:backend":  "npm run watch --workspace=backend",
    "dev:frontend": "npm run dev --workspace=frontend"
  }
}
Los scripts usan la flag --workspace=<nombre> de npm para delegar en el sub-paquete correspondiente. El campo workspaces le indica a npm qué directorios son paquetes del monorepo.
Es necesario configurar la variable de entorno OPENAI_API_KEY en backend/.env antes de arrancar el servidor. Sin ella, las peticiones al endpoint /ai/summary/:id fallarán con un error del proveedor de IA.

Rate Limiting

El sub-proyecto examples/ incluye una implementación manual de rate limiter para entender cómo funciona por dentro, sin depender de librerías externas:
// Rate limit en memoria usando un Map
const WINDOW_MS = 60 * 1000  // ventana de 1 minuto
const MAX_REQUESTS = 30       // máx. 30 peticiones por IP por minuto
const rateLimit = new Map()

// Limpia entradas expiradas cada minuto para evitar fugas de memoria
setInterval(() => {
  const now = Date.now()
  for (const [ip, entry] of rateLimit) {
    if (now - entry.start > WINDOW_MS) rateLimit.delete(ip)
  }
}, WINDOW_MS)

export function rateLimitMiddleware(req, res, next) {
  const ip = req.ip
  const now = Date.now()
  const entry = rateLimit.get(ip)

  if (!entry || now - entry.start > WINDOW_MS) {
    // Primera petición en la ventana: crea nueva entrada
    rateLimit.set(ip, { start: now, count: 1 })
    return next()
  }

  if (entry.count >= MAX_REQUESTS) {
    return res.status(429).json({
      error: 'Demasiadas peticiones. Intenta de nuevo más tarde.'
    })
  }

  entry.count++
  return next()
}

Cómo funciona el rate limiter manual

  • Usa un Map cuya clave es la IP del cliente y el valor es { start, count }.
  • Al llegar una petición, comprueba si existe entrada para esa IP y si la ventana de tiempo sigue activa.
  • Si el contador supera MAX_REQUESTS (30), responde con HTTP 429.
  • Un setInterval limpia las entradas expiradas cada minuto para evitar que el Map crezca indefinidamente.
El rate limiter de producción en routes/ai.js usa express-rate-limit (límite de 5 req/min/IP), más restrictivo que el ejemplo manual. Esto protege la API de costes elevados en el proveedor de IA.

Build docs developers (and LLMs) love