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étodo | Ruta | Descripción |
|---|
GET | /ai/summary/:id | Genera 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
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
| Paquete | Versión | Descripción |
|---|
express | ^5.2.1 | Servidor HTTP (versión 5 con soporte nativo async/await) |
ai | ^6.0.91 | Vercel AI SDK — streamText, generateText, etc. |
openai | ^6.22.0 | Cliente oficial de OpenAI para Node.js |
express-rate-limit | ^8.2.1 | Middleware de rate limiting por IP |
zod | ^4.3.5 | Validación de esquemas (usado en rutas y modelos) |
cors | ^2.8.5 | Middleware CORS configurable |
Arrancar el Proyecto
Ir al directorio del módulo
cd 07-inteligencia-artificial
Instalar todas las dependencias del monorepo
npm workspaces instala las dependencias de backend/ y frontend/ automáticamente. 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
Arrancar el backend
npm run dev:backend
# Equivale a: npm run watch --workspace=backend
# Servidor en http://localhost:1234
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.