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.
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.
- Leer desde el binding
env.MI_KVcongety elegir el tipo de retorno correcto:text,jsonostream. - Escribir valores de texto y binarios con
put, y borrarlos condelete. - Recorrer el espacio de claves con
list, usandoprefixy elcursorde 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:#11111blist 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.
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.
- Escribe un manejador que guarde un perfil como JSON con
put, lo recupere congetytype: "json", y lo borre condelete. Comprueba qué devuelvegetcuando la clave no existe. - Sirve un binario —una imagen— guardándolo desde el
bodyde unfetchy leyéndolo luego contype: "stream". Explica por quéstreames mejor quetextaquí. - Diseña un esquema de claves jerárquico para un carrito de la compra por usuario y escribe la función
listconprefixycursorque enumere todo lo de un usuario. - Reescribe dos lecturas seguidas —una del valor y otra de su metadata— como una sola llamada a
getWithMetadata. Razona qué ahorras. - Argumenta por qué KV no ofrece un
SELECT ... WHERE, conectándolo con la latencia que promete cadaget.