Registro con validación en vivo
14 chapters375 lines
No slides. A project.
Chapters
You leave with
You needNode.js 18+ · PostgreSQL corriendo localmente (o Docker) · psql CLI instalado · Cuenta y API key de Anthropic
←/→ 10s · 1-9 chapter · M mute · space play
03WHAT YOU'LL BUILD
8:11204 lines
Watch the classTener 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
output
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
output
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
output
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
output
CREATE TABLE
INSERT 0 4
04THE BENCH
14 chapters375 lines
14 chapters185 lines
14 chapters270 lines
05MOMENTS
El problema con los chatbots que alucinan
from Agente que consulta inventario en Postgres
Registro con validación en vivo
from Registro con validación en vivo
Vamos a construir un formulario de
from Un solo schema, cero inconsistencias: formulario de registro con Zod 4 + React Hook Form
Mis Notas
from Supabase RLS: Notas Privadas que SOLO tú puedes ver (sin backend propio)
El loop de tool use
from Agente que consulta inventario en Postgres
El problema real
from Registro con validación en vivo
El problema real
from Un solo schema, cero inconsistencias: formulario de registro con Zod 4 + React Hook Form
El problema: protección solo en la app
from Supabase RLS: Notas Privadas que SOLO tú puedes ver (sin backend propio)
mkdir agente-inventario && cd…
from Agente que consulta inventario en Postgres
Un schema, dos validaciones
from Registro con validación en vivo
Un schema, dos consumidores
from Un solo schema, cero inconsistencias: formulario de registro con Zod 4 + React Hook Form
La solución: RLS en la base de datos
from Supabase RLS: Notas Privadas que SOLO tú puedes ver (sin backend propio)
Crear la tabla y datos de prueba
from Agente que consulta inventario en Postgres
npm install react-hook-form zod…
from Registro con validación en vivo
npm install zod@^4.0.0 react-hook-form…
from Un solo schema, cero inconsistencias: formulario de registro con Zod 4 + React Hook Form
La referencia a auth.users y ENABLE ROW…
from Supabase RLS: Notas Privadas que SOLO tú puedes ver (sin backend propio)
06WHERE TO START
07WHO TEACHES
Agente que consulta inventario en Postgres
AI Talks generated presenter
08CLASS NOTES
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.
10NEXT CLASS
Vote or propose