19  10. Necesitamos conectar el lenguaje con Postgres

Drivers, pools y por qué no abres una conexión por request

NotaEn una frase

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:

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.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 objects

with 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.ts exporta el pool; nadie más crea conexiones.
  • max del pool < max_connections de Postgres (default 100) — deja margen para psql, 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

  1. Escribe la función createTask(userId, title) de tu stack con INSERT ... RETURNING parametrizada.
  2. Explica qué pasa, paso a paso, desde pool.query(...) hasta que vuelven las filas.
  3. Tu pool tiene max: 10 y llegan 50 requests simultáneos que hacen una query cada uno. ¿Qué ocurre?
  1. 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];
    }
  2. pool.query toma 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.

  3. 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í).

19.12 Lo que deberías saber hacer ahora