Registro con validación en vivo
14 capítulos375 líneas
No hay slides. Hay un proyecto.
Capítulos
Sales con esto
NecesitasNode.js 18+ · PostgreSQL corriendo localmente (o Docker) · psql CLI instalado · Cuenta y API key de Anthropic
←/→ 10s · 1-9 capítulo · M silencio · espacio play
03LO QUE VAS A CONSTRUIR
8:11204 líneas
Ver la claseTener una tabla real de productos con stock y precio para que el agente consulte datos verdaderos
schema.sql
CREATE TABLE IF NOT EXISTS products ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL, stock INTEGER NOT NULL DEFAULT 0, price NUMERIC(10,2) NOT NULL);INSERT INTO products (name, category, stock, price)VALUES ('Teclado mecanico', 'Perifericos', 34, 89.90), ('Monitor 27 pulgadas 4K', 'Monitores', 12, 349.00), ('Mouse inalambrico', 'Perifericos', 58, 29.50), ('Silla ergonomica', 'Muebles', 7, 219.00)ON CONFLICT DO NOTHING;psql -U postgres -d inventory -f schema.sql
salida
CREATE TABLE
INSERT 0 4
Crear un pool de conexiones reutilizable con manejo de errores de conexión
src/db.ts
import { Pool } from 'pg';import 'dotenv/config';export const pool = new Pool({ connectionString: process.env.DATABASE_URL,});pool.on('error', (err) => { console.error( 'Error en el pool de PG:', err );});Declarar el schema del tool que Claude puede llamar y la función que corre el SQL real con try/catch
src/tools.ts
import { pool } from './db';export const inventoryTool = { name: 'query_inventory', description: 'Consulta el inventario. Recibe una ' + 'categoria opcional y devuelve nombre, ' + 'stock y precio de los productos.', input_schema: { type: 'object' as const, properties: { category: { type: 'string', description: 'Categoria a filtrar', }, }, },};type Input = { category?: string };export async function runInventoryQuery( input: Input) { try { const { category } = input; const base = 'SELECT name, stock, price ' + 'FROM products'; const sql = category ? `${base} WHERE category ILIKE $1` : base; const values = category ? [category] : []; const { rows } = await pool.query( sql, values ); if (rows.length === 0) { return { ok: false, error: 'No hay productos en esa ' + 'categoria.', }; } return { ok: true, data: rows }; } catch (err) { console.error('Error en PG:', err); return { ok: false, error: 'La base de datos no respondio.', }; }}Entender cómo Claude pide ejecutar el tool, cómo se le devuelve el resultado y cómo se sigue el loop hasta la respuesta final
src/agent.ts
import Anthropic from '@anthropic-ai/sdk';import { inventoryTool, runInventoryQuery,} from './tools';const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY,});export async function askAgent( question: string) { const messages: any[] = [ { role: 'user', content: question }, ]; let response = await anthropic.messages.create({ model: 'claude-sonnet-4-5', max_tokens: 1024, tools: [inventoryTool], messages, }); while (response.stop_reason === 'tool_use') { const toolUse = response.content.find( (b: any) => b.type === 'tool_use' ); if (!toolUse) break; const result = await runInventoryQuery( toolUse.input as { category?: string } ); messages.push({ role: 'assistant', content: response.content, }); messages.push({ role: 'user', content: [ { type: 'tool_result', tool_use_id: toolUse.id, content: JSON.stringify(result), }, ], }); response = await anthropic.messages.create({ model: 'claude-sonnet-4-5', max_tokens: 1024, tools: [inventoryTool], messages, }); } const finalText = response.content.find( (b: any) => b.type === 'text' ); return finalText ? finalText.text : '';}Crear un endpoint Express que recibe la pregunta, llama al agente y devuelve un error controlado si algo falla
src/server.ts
import express from 'express';import 'dotenv/config';import { askAgent } from './agent';const app = express();app.use(express.json());app.use(express.static('public'));app.post('/api/chat', async (req, res) => { const { question } = req.body; if (!question) { return res.status(400).json({ error: 'Falta la pregunta', }); } try { const answer = await askAgent(question); res.json({ answer }); } catch (err) { console.error('Error del agente:', err); res.status(500).json({ error: 'El agente no pudo responder.', }); }});const PORT = process.env.PORT || 3000;app.listen(PORT, () => { console.log(`Server en puerto ${PORT}`);});npx ts-node src/server.ts
salida
Server en puerto 3000
Ver el agente respondiendo con datos reales y comprobar que un caso sin resultados o con error de conexión se explica en lenguaje natural en vez de romper la app
public/index.html
<!DOCTYPE html><html lang="es"><head> <meta charset="UTF-8" /> <title>Agente de Inventario</title></head><body> <h1>Agente de Inventario</h1> <input id="q" placeholder="Pregunta por el stock..." /> <button id="send">Enviar</button> <p id="answer"></p> <script> const send = document.getElementById('send'); send.onclick = async () => { const q = document.getElementById('q').value; const res = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ question: q, }), }); const data = await res.json(); document.getElementById( 'answer' ).innerText = data.answer || data.error; }; </script></body></html>npx ts-node src/server.ts
salida
Server en puerto 3000
Pregunta por el stock...
Enviar
Monitor 27 pulgadas 4K: 12 unidades a $349.00
No hay productos en esa categoria.
CREATE TABLE IF NOT EXISTS products ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL, stock INTEGER NOT NULL DEFAULT 0, price NUMERIC(10,2) NOT NULL);INSERT INTO products (name, category, stock, price)VALUES ('Teclado mecanico', 'Perifericos', 34, 89.90), ('Monitor 27 pulgadas 4K', 'Monitores', 12, 349.00), ('Mouse inalambrico', 'Perifericos', 58, 29.50), ('Silla ergonomica', 'Muebles', 7, 219.00)ON CONFLICT DO NOTHING;psql -U postgres -d inventory -f schema.sql
salida
CREATE TABLE
INSERT 0 4
04LA MESA
14 capítulos375 líneas
14 capítulos185 líneas
14 capítulos270 líneas
05MOMENTOS
El problema con los chatbots que alucinan
de Agente que consulta inventario en Postgres
Registro con validación en vivo
de Registro con validación en vivo
Vamos a construir un formulario de
de Un solo schema, cero inconsistencias: formulario de registro con Zod 4 + React Hook Form
Mis Notas
de Supabase RLS: Notas Privadas que SOLO tú puedes ver (sin backend propio)
El loop de tool use
de Agente que consulta inventario en Postgres
El problema real
de Registro con validación en vivo
El problema real
de Un solo schema, cero inconsistencias: formulario de registro con Zod 4 + React Hook Form
El problema: protección solo en la app
de Supabase RLS: Notas Privadas que SOLO tú puedes ver (sin backend propio)
mkdir agente-inventario && cd…
de Agente que consulta inventario en Postgres
Un schema, dos validaciones
de Registro con validación en vivo
Un schema, dos consumidores
de Un solo schema, cero inconsistencias: formulario de registro con Zod 4 + React Hook Form
La solución: RLS en la base de datos
de Supabase RLS: Notas Privadas que SOLO tú puedes ver (sin backend propio)
Crear la tabla y datos de prueba
de Agente que consulta inventario en Postgres
npm install react-hook-form zod…
de Registro con validación en vivo
npm install zod@^4.0.0 react-hook-form…
de Un solo schema, cero inconsistencias: formulario de registro con Zod 4 + React Hook Form
La referencia a auth.users y ENABLE ROW…
de Supabase RLS: Notas Privadas que SOLO tú puedes ver (sin backend propio)
06POR DÓNDE ENTRAR
07QUIÉN DICTA
Agente que consulta inventario en Postgres
Presentador generado por AI Talks
08APUNTES
Serializa SIEMPRE el resultado del tool con JSON.stringify, incluso cuando es un error ({ ok: false, error: '...' }). Claude interpreta mejor un string estructurado y consistente que un objeto crudo o un mensaje de error suelto, y eso reduce muchísimo los casos donde el modelo se queda repitiendo la misma pregunta o inventa una respuesta genérica en vez de explicar la falla real.
10LA PRÓXIMA CLASE
Vota o propón