wandres.dev
KV · clave-valor global

La API de KV: get, put, delete y list

La superficie de Workers KV cabe en cuatro verbos que operan siempre sobre el binding declarado en tu manifiesto, ese objeto env.MI_KV que representa el namespace. Vemos get y sus tipos de retorno —text, json y stream, cada uno para un tamaño y un uso distinto—, put para escribir valores de texto o binarios, delete para borrar, list para recorrer el espacio de claves por prefijo con paginación por cursor, y getWithMetadata para leer valor y contexto en una sola llamada. Cuatro operaciones asíncronas que, bien entendidas, cubren todo lo que KV sabe hacer.

⏱ 15 min

La API de KV es minúscula y esa es su virtud: no hay lenguaje de consulta, ni transacciones, ni índices que mantener. Hay un objeto —el binding env.MI_KV, que apunta al namespace declarado en wrangler.jsonc— y sobre él cuatro métodos asíncronos: get para leer, put para escribir, delete para borrar y list para enumerar claves. Toda la potencia y todos los límites de KV se expresan en esa superficie diminuta. Dominarla es sobre todo entender los detalles que no saltan a la vista: qué tipo pides al leer, qué formatos admite el valor al escribir, y por qué list no es una consulta sino un recorrido paginado.

🎯 Al terminar esta lección sabrás
  • Leer desde el binding env.MI_KV con get y elegir el tipo de retorno correcto: text, json o stream.
  • Escribir valores de texto y binarios con put, y borrarlos con delete.
  • Recorrer el espacio de claves con list, usando prefix y el cursor de paginación.
  • Recuperar valor y metadata a la vez con getWithMetadata, evitando una segunda lectura.

Leer con get y sus tipos

get recibe una clave y devuelve una promesa con su valor, o null si la clave no existe. El detalle que marca la diferencia es el segundo argumento: el tipo en que quieres el valor. Por defecto es text, que te da una cadena; json deserializa el valor y te devuelve el objeto ya parseado; stream te entrega un ReadableStream para valores grandes que prefieres no cargar enteros en memoria; y arrayBuffer te da los bytes crudos.

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // texto plano: el retorno es string o null
    const nombre = await env.MI_KV.get("usuario:42:nombre");

    // json: KV deserializa y te da el objeto directamente
    const perfil = await env.MI_KV.get("usuario:42", { type: "json" });

    // stream: para valores grandes, sin cargarlos enteros en memoria
    const imagen = await env.MI_KV.get("logo.png", { type: "stream" });

    return Response.json({ nombre, perfil, tieneImagen: imagen !== null });
  },
};

La elección del tipo no es cosmética. Pedir json te ahorra el JSON.parse manual pero falla si el valor no es JSON válido. Pedir stream o arrayBuffer es la forma correcta de servir binarios —una imagen, un PDF cacheado— sin materializarlos en memoria, algo que importa cuando te acercas al límite de 25 MiB por valor. La regla práctica: text para cadenas, json para objetos que tú mismo serializaste, stream para lo grande y binario.

Escribir con put y borrar con delete

put toma una clave y un valor. El valor puede ser una cadena, un ArrayBuffer o un ReadableStream, así que puedes guardar tanto texto y JSON serializado como binarios. Un tercer argumento opcional lleva la configuración de expiración y metadata que veremos en la siguiente lección. delete es aún más simple: recibe la clave y la borra, sin quejarse si no existía.

// guardar texto o JSON (serializado a mano)
await env.MI_KV.put("usuario:42:nombre", "Ada");
await env.MI_KV.put("usuario:42", JSON.stringify({ nombre: "Ada", plan: "pro" }));

// guardar binario desde un ReadableStream (por ejemplo, el body de un fetch)
const origen = await fetch("https://ejemplo.com/logo.png");
await env.MI_KV.put("logo.png", origen.body);

// borrar
await env.MI_KV.delete("usuario:42:nombre");

Aquí conviene recordar la asimetría del nivel: get es la operación barata y put la cara. Escribir la misma clave muchas veces por segundo no solo es lento en propagar, sino que está desaconsejado —KV admite del orden de una escritura por segundo y clave—. Si te descubres llamando a put en un bucle caliente sobre la misma clave, la herramienta correcta no es KV.

Enumerar con list

list no es una consulta: es un recorrido del espacio de claves. Devuelve las claves —no los valores— ordenadas lexicográficamente, filtradas opcionalmente por un prefix, y en páginas de como mucho 1000 elementos. Cuando hay más, el resultado trae list_complete: false y un cursor con el que pides la página siguiente. Cada clave listada incluye su nombre, su expiración si la tiene y su metadata.

async function todasLasClaves(env: Env, prefijo: string): Promise<string[]> {
  const claves: string[] = [];
  let cursor: string | undefined;
  do {
    const pagina = await env.MI_KV.list({ prefix: prefijo, cursor });
    for (const k of pagina.keys) claves.push(k.name);
    cursor = pagina.list_complete ? undefined : pagina.cursor;
  } while (cursor);
  return claves;
}

El diseño del prefijo es tu único “índice”. Nombrando las claves con jerarquía —usuario:42:sesion, usuario:42:carrito— puedes listar todo lo de un usuario con prefix: "usuario:42:". Y cuando necesitas el valor junto a su contexto, getWithMetadata te da ambos en una sola lectura, sin una segunda ida al almacén:

const { value, metadata } = await env.MI_KV.getWithMetadata("usuario:42", {
  type: "json",
});
flowchart LR
L[list con prefix] --> P[pagina de hasta 1000 claves]
P --> Q{list_complete}
Q -- si --> FIN[recorrido terminado]
Q -- no --> N[pide de nuevo con el cursor]
N --> P
style P fill:#89b4fa,color:#11111b
style FIN fill:#a6e3a1,color:#11111b
style Q fill:#fab387,color:#11111b
⚠️
list no es tiempo real ni una base de datos

list hereda la consistencia eventual de KV: la lista de claves que ves puede no reflejar las escrituras de los últimos segundos, y recorrer millones de claves con paginación es lento y caro. No lo uses para contar en vivo, para paginar una interfaz cara al usuario ni como sustituto de una consulta relacional. Si necesitas filtrar por campos, ordenar por valor o contar con exactitud, ese trabajo es de D1, no de list.

El binding es una capacidad, y la API pequeña es una elección, no una carencia

Fíjate en que nunca escribes una URL, ni una cadena de conexión, ni una clave de API para hablar con KV: escribes env.MI_KV. Ese objeto es un binding, y un binding es una capacidad en el sentido técnico del término —una referencia que a la vez nombra un recurso y otorga permiso para usarlo—. No hay credenciales que filtrar porque no hay credenciales: el derecho a tocar ese namespace está grabado en el propio objeto que la plataforma inyecta, y sin él no hay forma de llegar al almacén. Esta es una idea que reaparece en toda la plataforma y que conviene ver como el patrón que es: seguridad por construcción en vez de por comprobación. Pero hay una segunda lección, más sutil, escondida en el tamaño de la API. Cuatro verbos y ningún lenguaje de consulta no es una limitación que Cloudflare no tuvo tiempo de superar: es la superficie exacta que puede sostenerse con las garantías que KV promete. Un get por clave se puede servir desde una copia local en un milisegundo; un SELECT ... WHERE ... ORDER BY no, porque exigiría o bien mover toda la base de datos a cada edge o bien viajar al origen y perder justo la latencia que da sentido a KV. La pobreza de la API es, por tanto, la huella de su física: cada operación que existe puede cumplirse rápido y en todas partes, y cada operación que no existe se omitió precisamente porque no podía. Cuando internalizas esto dejas de preguntar “¿cómo hago un join en KV?” —la pregunta ya está mal planteada— y empiezas a preguntar “¿qué forma debe tener mi clave para que un simple get me dé lo que un join me daría en otro sistema?”. Modelar en KV es diseñar claves, no consultas; y ese giro, de la consulta al esquema de nombres, es el corazón del oficio con almacenes clave-valor.

⚔️ Domina los cuatro verbos
  1. Escribe un manejador que guarde un perfil como JSON con put, lo recupere con get y type: "json", y lo borre con delete. Comprueba qué devuelve get cuando la clave no existe.
  2. Sirve un binario —una imagen— guardándolo desde el body de un fetch y leyéndolo luego con type: "stream". Explica por qué stream es mejor que text aquí.
  3. Diseña un esquema de claves jerárquico para un carrito de la compra por usuario y escribe la función list con prefix y cursor que enumere todo lo de un usuario.
  4. Reescribe dos lecturas seguidas —una del valor y otra de su metadata— como una sola llamada a getWithMetadata. Razona qué ahorras.
  5. Argumenta por qué KV no ofrece un SELECT ... WHERE, conectándolo con la latencia que promete cada get.