sequenceDiagram
participant H as Handler
participant P as Pool (10 conexiones)
participant DB as PostgreSQL
H->>P: query("SELECT ... WHERE user_id=$1", [42])
P->>DB: toma conexión libre, envía query + params por separado
DB-->>P: filas
P-->>H: filas → Task[] (mapeo a tu estructura)
Note over P: la conexión vuelve al pool, no se cierra
19 10. Necesitamos conectar el lenguaje con Postgres
Drivers, pools y por qué no abres una conexión por request
Tu código y Postgres hablan por cable de red — el driver es el traductor, el pool es el grupo de conexiones reutilizables, y los parámetros son cómo envías datos sin inyección. Al final del capítulo Taskflow guarda tareas de verdad: reinicias el servidor y siguen ahí.
19.1 El problema
Las queries del cap. 9 existen en psql, escritas a mano. Pero el que necesita ejecutarlas es tu código — cuando llega GET /tasks, alguien tiene que traducir listTasks() a SELECT ... WHERE user_id = $1, enviarla por la red, y convertir las filas que vuelven en objetos de tu lenguaje. Ese “alguien” es el driver — y tiene trampas que no se ven en el tutorial de 5 líneas.
Un objeto es una colección de datos con nombre: {name: "Ana", age: 30} — cada dato es una propiedad (clave → valor). Python los llama dict, Go los arma con struct, TS con objetos/interface.
Una query es la pregunta que le haces a la base de datos en SQL: SELECT * FROM tasks WHERE done = false. La BD traduce la pregunta a un plan de búsqueda — por eso los índices importan.
19.2 Cómo lo resuelve un equipo
La pieza conceptual nueva es el pool de conexiones. Abrir una conexión a Postgres cuesta: handshake TCP, autenticación, proceso nuevo en el servidor (~50ms y memoria). Si cada request abriera y cerrara una, la mitad de tu latencia sería conectar. El pool mantiene N conexiones abiertas y las presta: pool.query() toma una libre, la usa, la devuelve. Con 10 conexiones atiendes cientos de requests — porque un request usa la conexión unos milisegundos.
return hace dos cosas a la vez: devuelve el resultado Y termina la función — lo que esté debajo nunca corre. return task = “aquí está el plato, salgo de la cocina”.
Un proceso es un programa en ejecución: el sistema operativo le da memoria propia y un lugar en la fila del CPU. Tu API es un proceso; Postgres es otro; el navegador es varios. Que “se caiga el proceso” = el programa murió.
La memoria RAM es el escritorio de trabajo del programa: rápida, pero se borra al apagar. Los datos que deben sobrevivir van a disco o a la base de datos. Por eso let tasks = [] pierde todo al reiniciar.
Un pool es el staff de conexiones a la BD: abrir una conexión por request es caro, así que el pool mantiene ~10–20 abiertas y las presta. El request la usa, la devuelve, y la siguiente la reutiliza.
La autenticación responde “¿quién eres?” — login, contraseña, token. Se confunde con autorización (“¿qué puedes hacer?”), que es la pregunta siguiente. Primero te identificas, luego te dejan o no pasar.
El flujo completo del capítulo:
19.3 Conceptos nuevos
- D Driver:
pg/psycopg/pgx— todos hablan el mismo protocolo de Postgres - D Pool de conexiones: N conexiones abiertas prestadas por milisegundos — nunca una por request
- D Parámetros
$1/%s: el dato viaja separado del SQL (la regla del cap. 9, ahora en código) - U Mapear filas a estructuras: objeto vs clase vs
Scan()a struct - U ORM como opción: Prisma / SQLAlchemy / sqlc — y por qué el libro usa driver crudo primero
19.4 La explicación visual
| Capa | Pieza | Análogo |
|---|---|---|
| Protocolo | Postgres wire protocol | El idioma del cable (igual para todos) |
| Driver | pg, psycopg, pgx |
El traductor: objetos ↔︎ protocolo |
| Pool | pg.Pool, psycopg_pool, pgxpool |
La flota de taxis: los prestas, no los compras |
| Tu código | query(sql, params) → structs |
El que pide y recibe |
19.5 Implementación
Taskflow persiste de verdad — listTasks con driver crudo en los tres stacks. Fíjate en las tres constantes: pool creado una vez, params separados, filas mapeadas a tu estructura.
// db.ts — the pool is created ONCE, at startup
import { Pool } from "pg";
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10, // at most 10 open connections
});
// tasks.ts — service layer uses the pool
export async function listTasks(userId: number): Promise<Task[]> {
const { rows } = await pool.query(
`SELECT public_id, title, priority, due_date, done
FROM tasks WHERE user_id = $1
ORDER BY priority ASC`, // $1 — parameter, never `${userId}`
[userId],
);
return rows; // pg already maps rows → objects
}pg convierte cada fila en un objeto plano — el mapeo es automático, pero los nombres de columna son tu contrato (public_id, no id).
# db.py — pool created once at startup
from psycopg_pool import ConnectionPool
pool = ConnectionPool(
conninfo=os.environ["DATABASE_URL"],
max_size=10,
)
# tasks.py — service layer
def list_tasks(user_id: int) -> list[Task]:
with pool.connection() as conn: # borrow one
rows = conn.execute(
"""SELECT public_id, title, priority, due_date, done
FROM tasks WHERE user_id = %s -- %s — parameter
ORDER BY priority ASC""",
(user_id,),
).fetchall()
return [Task(**dict(r)) for r in rows] # rows → Task objectswith pool.connection() presta la conexión y la devuelve sola — el context manager de Python haciendo lo suyo (como with open()).
// db.go — pool created once at startup
pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
// pgxpool defaults to sensible max connections
// tasks.go — service layer
func listTasks(ctx context.Context, userID int64) ([]Task, error) {
rows, err := pool.Query(ctx,
`SELECT public_id, title, priority, due_date, done
FROM tasks WHERE user_id = $1
ORDER BY priority ASC`, userID) // $1 — parameter
if err != nil {
return nil, err
}
defer rows.Close() // return the connection
// pgx maps each row into the struct via Scan / CollectRows
return pgx.CollectRows(rows, pgx.RowToStructByName[Task])
}Go explicita todo: ctx para cancelación, defer rows.Close() para devolver la conexión, y el mapeo a Task hecho por CollectRows.
Lo que cambió en Taskflow: storage/tasks.ts ya no es un array — es el archivo que llama al pool. El handler no se enteró (cap. 7 pagando dividendos): listTasks(userId) devuelve Task[] igual que antes; solo cambia dónde viven los datos.
Un array es una colección ordenada de elementos accedidos por posición: ["a","b","c"][0] es "a" (se cuenta desde 0). Python las llama listas, Go slices — misma idea, distinto acento.
Conecta mi API de tareas [Express/FastAPI/Go] a PostgreSQL con el driver
crudo [pg / psycopg+pool / pgx]. Requisitos: pool de conexiones creado
UNA vez al arrancar con max ~10, DATABASE_URL desde variable de entorno,
todas las queries parametrizadas ($1/%s — nunca concatenar), la capa de
storage con las 4 funciones del CRUD (list/create/get/delete filtrando
por user_id), y las filas mapeadas a mis structs existentes. El handler
debe quedar idéntico — solo cambia el storage. Explícame por qué el pool
va en un archivo separado y qué pasa si lo creo por request.
19.6 ¿Por qué cada stack lo hace así?
Lo único que necesitas llevarte: los tres drivers hacen exactamente lo mismo — pool, query parametrizada, filas→estructura. Cambia la ceremonia (context managers, defer, async/await) no el modelo. Aprende uno y los otros dos son un fin de semana.
| Alternativa | Qué es | Qué te cuesta | Cuándo elegirla |
|---|---|---|---|
| Driver crudo | Escribes SQL tú | Más líneas, pero control total | Este libro; queries que importan; aprendizaje |
| Query builder (Knex, Kysely, sqlc) | SQL con autocompletado | Otra capa que aprender | Equipo que quiere SQL con tipos sin magia |
| ORM (Prisma, SQLAlchemy, GORM) | Objetos que generan SQL | La query real se esconde; N+1 silencioso (cap. 12) | CRUDs simples, equipos que ya lo dominan |
| sqlc (Go) | Escribes SQL, genera código Go tipado | Solo en Go | El punto dulce del ecosistema Go |
El orden del libro no es casual: SQL → driver → (opcional) ORM. Un ORM sobre entendimiento es atajo; sobre ignorancia, es deuda.
19.7 Errores comunes
| Error | Por qué pasa | Fix |
|---|---|---|
| Pool creado por request | new Pool() dentro del handler → 10 conexiones nuevas por request |
Pool global, creado en el arranque |
f"WHERE id = {id}" |
El f-string se ve inocente | $1/%s + array de params — siempre |
| No devolver la conexión | rows sin cerrar / conn sin liberar → pool agotado y app colgada |
with/defer/el driver devuelve tras await |
El Task del código ≠ la fila |
Mapeaste id en vez de public_id y expusiste el interno |
SELECT nombra columnas; el struct usa public_id |
| Errores de BD llegando al cliente | El stack de Postgres en el JSON de error | El error mapper del cap. 6 los traduce a 500 genérico |
19.8 Buenas prácticas
- Un pool, un archivo, una instancia —
db.tsexporta el pool; nadie más crea conexiones. maxdel pool <max_connectionsde Postgres (default 100) — deja margen parapsql, migraciones y la réplica futura.
Una migración es un cambio versionado del esquema: un archivo que dice “crea esta columna” (up) y “bórrala” (down). Son el historial Git de la estructura de la BD — se aplican en orden y no se editan una vez aplicadas.
- Toda query parametrizada, sin excepciones — incluso “valores que yo controlo”: el día que deje de controlarlos no habrá alerta.
- Cierra el pool al apagar (
pool.end()en el shutdown del servidor) — las conexiones zombie son fugas reales.
19.9 Ejercicio
- Escribe la función
createTask(userId, title)de tu stack conINSERT ... RETURNINGparametrizada. - Explica qué pasa, paso a paso, desde
pool.query(...)hasta que vuelven las filas. - Tu pool tiene
max: 10y llegan 50 requests simultáneos que hacen una query cada uno. ¿Qué ocurre?
TypeScript:
export async function createTask(userId: number, title: string) { const { rows } = await pool.query( `INSERT INTO tasks (user_id, title) VALUES ($1, $2) RETURNING public_id, title, priority, done, created_at`, [userId, title], ); return rows[0]; }pool.querytoma una conexión libre del pool (o espera si todas están ocupadas) → el driver serializa query+params en el protocolo de Postgres → la BD ejecuta → devuelve filas → el driver las convierte a objetos → la conexión vuelve al pool → tu función retorna.Los primeros 10 toman las conexiones; los otros 40 esperan en cola dentro del pool (no fallan) y van entrando conforme se liberan — cada query dura milisegundos, así que el drenaje es rápido. Por eso 10 conexiones atienden cientos de usuarios.
19.10 Mini reto
Detectar la diferencia entre “objeto modificado en memoria” y “fila actualizada en BD” — este código compila y parece funcionar:
Un compilador traduce código a otra forma: TypeScript → JavaScript (transpila), Go → binario (compila). Atrapa errores antes de ejecutar. tsc, esbuild, go build son compiladores.
Una base de datos es el programa que guarda datos de forma permanente y los responde rápido. Relacional (Postgres): tablas con relaciones. No relacional: documentos, clave-valor, grafos — cada una para una forma de dato distinta.
task = get_task(42) # reads the row into an object
task.done = True # updates... what exactly?
return task # the API says "done", but...task.done = True solo cambió el objeto en memoria — la fila en Postgres sigue intacta. El siguiente request lee done = FALSE otra vez. Este es el bug fantasma de quien viene de ORMs que “sincronizan”: con driver crudo tú eres la sincronización — falta el UPDATE tasks SET done=TRUE WHERE id=$1. Regla mental: el objeto es una foto de la fila, no la fila — editar la foto no edita el archivo.
19.11 Vocabulario técnico del capítulo
Una variable es una caja con nombre donde guardas un valor: let total = 42 guarda el 42 bajo el nombre total. Puedes leerla y cambiarla después (total = 50). const = caja que no se puede reemplazar.
Una función es una receta reutilizable: recibe ingredientes (parámetros), hace pasos y devuelve un plato (return). La escribes una vez y la llamas mil veces: add(2, 3) → 5.
El parámetro es el hueco declarado en la función (def f(x) — x es parámetro); el argumento es el valor concreto que le pasas (f(42) — 42 es argumento). Mismo dato, dos momentos: declaración vs uso.
Un bucle repite una acción por cada elemento o hasta cumplir una condición: for task in tasks hace algo con cada tarea. map y filter son bucles disfrazados de funciones — transforman/filtran colecciones sin for explícito.
Una clase es el molde de un objeto: define qué datos y qué métodos tiene. class Task es el molde; new Task() es una instancia concreta. Go no tiene clases — usa struct + métodos sueltos.
Un string es texto entre comillas: "hola". El nombre viene de “cadena de caracteres” — una secuencia de letras. Todo lo que llega de un formulario o una URL llega como string, aunque parezca número.
null (TS), None (Py), nil (Go) = “aquí no hay valor”. Es la respuesta a “¿qué devuelvo cuando no hay nada?” — y la fuente del bug más famoso de la historia (su inventor lo llamó “el error del billón de dólares”). Por eso el código revisa if x is not None.
Un entorno es una instancia completa donde corre tu app con su propia config y datos: local (tu máquina), staging (réplica de prueba), producción (la real). Cada entorno tiene sus propias llaves y su propia base de datos.
Un ORM (Object-Relational Mapper) traduce entre objetos del código y filas de la tabla: task.save() en vez de INSERT. Cómodo para el CRUD, peligroso si no sabes qué SQL genera (el N+1 nace ahí).