Web Crypto API en el browser vs Node.js: las diferencias que te van a quemar
En 2021, cuando estaba pasando del mundo Java al mundo TypeScript/Node.js, había una convicción que traía conmigo: "los estándares web son estándares, punto". Si algo se llama SubtleCrypto en el browser, tiene que comportarse igual en Node.js, ¿no? La respuesta corta es: no exactamente. La respuesta larga es este post.
Mi tesis: crypto.subtle parece una API unificada hasta que intentás reutilizar el mismo código de cifrado entre browser, Node.js 20+ y el edge runtime de Next.js. Las diferencias no son filosóficas —son concretas, están documentadas en MDN y en los docs oficiales de Node.js, y aparecen en los peores momentos: cuando el código ya está mezclado en un módulo compartido.
Web Crypto API en browser, Node.js y edge: un "estándar" con tres sabores
La Web Crypto API define una interfaz para operaciones criptográficas en el browser. Node.js implementó su propia versión bajo globalThis.crypto a partir de v17, y la consideró estable en v19. Desde Node.js 20 está disponible globalmente sin necesidad de import.
El edge runtime de Next.js es un tercer entorno: V8-based, sin acceso a las APIs de Node.js nativas, con un subconjunto explícito de Web APIs disponibles según los docs oficiales de Next.js Edge Runtime.
En teoría, los tres exponen crypto.subtle. En la práctica, los tres tienen diferencias de superficie que importan cuando el código es compartido.
El acceso al objeto crypto
// Browser: global sin import
const key = await crypto.subtle.generateKey(/* ... */);
// Node.js 20+ — también global, sin import requerido
// pero en versiones anteriores a la 19, era así:
import { webcrypto } from 'node:crypto';
const key = await webcrypto.subtle.generateKey(/* ... */);
// Edge Runtime (Next.js Middleware, Route Handlers con `export const runtime = 'edge'`)
// crypto.subtle está disponible — pero no todas las operaciones están garantizadas
const key = await crypto.subtle.generateKey(/* ... */);El problema no es el acceso —es que tres entornos con la misma superficie de API no soportan exactamente el mismo conjunto de algoritmos ni los mismos parámetros en cada operación.
Las diferencias concretas que nadie lee hasta que algo falla
1. Algoritmos disponibles: no todos están en los tres entornos
La especificación W3C define un conjunto de algoritmos para SubtleCrypto. Node.js los implementa según su propia versión de OpenSSL interna. El edge runtime tiene restricciones adicionales por su entorno V8-only.
Según la documentación de Node.js Web Crypto, algunos algoritmos como Ed25519 y X25519 fueron marcados como estables en versiones específicas de Node.js. Si tu código corre en Node.js 18 y en un edge runtime que no tiene esos algoritmos, el mismo generateKey con { name: 'Ed25519' } puede funcionar en uno y tirar DOMException: Unrecognized name en el otro.
// Esto puede fallar silenciosamente en edge runtimes más restrictivos
// Verificá siempre contra: https://nextjs.org/docs/app/api-reference/edge
const keyPair = await crypto.subtle.generateKey(
{
name: 'Ed25519', // ← algoritmo que NO está garantizado en edge
},
true,
['sign', 'verify']
);Para operaciones de cifrado simétrico, AES-GCM es el algoritmo más portátil entre los tres entornos. Es el que tiene mejor cobertura documentada tanto en MDN como en la implementación de Node.js.
// AES-GCM: el que mejor viaja entre browser, Node.js y edge
async function generarClave(): Promise<CryptoKey> {
return crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256, // 128 o 256 bits — ambos soportados
},
true, // extractable: necesario para exportar/importar entre contextos
['encrypt', 'decrypt']
);
}
async function cifrar(
clave: CryptoKey,
datos: string
): Promise<{ cifrado: ArrayBuffer; iv: Uint8Array }> {
const iv = crypto.getRandomValues(new Uint8Array(12)); // 12 bytes para GCM
const encoder = new TextEncoder();
const cifrado = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
clave,
encoder.encode(datos)
);
return { cifrado, iv };
}2. crypto.getRandomValues vs crypto.randomBytes: no son intercambiables
Este es el error más común que aparece cuando alguien migra código de Node.js al browser o al edge.
// ❌ Esto es Node.js nativo — NO disponible en browser ni en edge runtime
import { randomBytes } from 'node:crypto';
const iv = randomBytes(12);
// ✅ Esto SÍ funciona en los tres entornos
const iv = crypto.getRandomValues(new Uint8Array(12));randomBytes es de la API nativa de Node.js (node:crypto), no de la Web Crypto API. En un módulo compartido entre Next.js App Router (server components), Middleware (edge) y código de cliente, ese import explota en silencio o con un error de módulo no encontrado.
3. Exportación e importación de claves: el formato importa
Cuando necesitás persistir una clave o pasarla entre contextos, crypto.subtle.exportKey y importKey trabajan con formatos específicos. El error acá no es de entorno —es de formato de key.
// Exportar una clave AES para guardarla (ej: en sessionStorage o en Redis)
async function exportarClave(clave: CryptoKey): Promise<string> {
const raw = await crypto.subtle.exportKey('raw', clave);
// Convertir a base64 para serialización
return btoa(String.fromCharCode(...new Uint8Array(raw)));
}
// Importar de vuelta desde base64
async function importarClave(base64: string): Promise<CryptoKey> {
const raw = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
return crypto.subtle.importKey(
'raw',
raw,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
}Lo que cambia entre entornos: btoa y atob son globales en browser y en edge. En Node.js, btoa/atob son globales desde v16, pero si algún módulo en la cadena asume que no existen y usa Buffer.from(...).toString('base64'), tenés inconsistencia silenciosa en la serialización.
4. Edge runtime de Next.js: el subconjunto que duele
El Edge Runtime de Next.js documenta explícitamente qué APIs están disponibles. crypto.subtle aparece en la lista, pero con la advertencia de que el entorno V8 isolate tiene restricciones.
Lo que esto significa para Middleware o Route Handlers con runtime = 'edge':
// app/api/token/route.ts con edge runtime
export const runtime = 'edge';
export async function POST(req: Request) {
// ✅ Esto funciona en edge
const iv = crypto.getRandomValues(new Uint8Array(12));
// ✅ AES-GCM funciona en edge
const clave = await crypto.subtle.importKey(
'raw',
/* buffer de 32 bytes */,
{ name: 'AES-GCM' },
false,
['encrypt']
);
// ❌ NO importes 'node:crypto' acá — el edge runtime no tiene Node APIs
// import { createCipheriv } from 'node:crypto'; // Error en runtime
}La regla práctica: si el Route Handler o el Middleware corre en edge, usá exclusivamente la Web Crypto API (crypto.subtle, crypto.getRandomValues). Nada de node:crypto.
Los errores que aparecen cuando mezclás entornos
Error 1: módulo compartido que importa node:crypto
El escenario más común en un monorepo Next.js: una función de cifrado en lib/crypto.ts que usa node:crypto para aprovechar randomBytes o createCipheriv. Esa función viaja sin problema a un Server Component o a un API route con Node.js runtime. Pero si algún día la usás en Middleware o en un Route Handler con runtime = 'edge', el build compila y el runtime explota.
// ❌ lib/crypto.ts — NO portable a edge
import { randomBytes, createCipheriv } from 'node:crypto';
// ✅ lib/crypto-portable.ts — funciona en los tres entornos
// Solo usa Web Crypto API
export async function generarIV(): Promise<Uint8Array> {
return crypto.getRandomValues(new Uint8Array(12));
}Error 2: asumir que los ArrayBuffer son iguales en todos lados
crypto.subtle.encrypt devuelve un ArrayBuffer. En Node.js, podés hacer Buffer.from(arrayBuffer) para convertirlo. En browser y edge, Buffer no existe. Si el código downstream asume Buffer, falla en browser.
// ✅ Portable — usa Uint8Array, no Buffer
function arrayBufferAHex(buffer: ArrayBuffer): string {
return Array.from(new Uint8Array(buffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
// ❌ Solo Node.js
// Buffer.from(buffer).toString('hex');Error 3: SubtleCrypto.digest para hashing — cuidado con SHA-1
crypto.subtle.digest soporta SHA-1, SHA-256, SHA-384 y SHA-512 en los tres entornos. SHA-1 está ahí por compatibilidad pero no debería usarse para nada nuevo. El error no es de entorno —es de elección de algoritmo. Si alguien hereda código que usa SHA-1 en digest, funciona en todos lados y ese es exactamente el problema.
// ✅ SHA-256 — portable y seguro para hashing
async function hashearTexto(texto: string): Promise<string> {
const encoder = new TextEncoder();
const datos = encoder.encode(texto);
const hash = await crypto.subtle.digest('SHA-256', datos);
return arrayBufferAHex(hash);
}Checklist de decisión: antes de escribir código criptográfico compartido
Antes de crear un módulo de cifrado que va a cruzar entornos, pasá por esto:
¿Dónde va a correr este código?
- Solo browser → podés usar
crypto.subtlesin restricciones documentadas - Solo Node.js (Server Components, API routes sin edge) → podés usar
crypto.subtleglobal onode:cryptonativo, pero no mezcles - Edge runtime (Middleware, Route Handler con
runtime = 'edge') → solocrypto.subtleycrypto.getRandomValues, cero imports denode:crypto - Módulo compartido entre dos o más de los anteriores → la restricción más fuerte gana: Web Crypto API pura
¿Qué algoritmo?
- Para cifrado simétrico:
AES-GCMcon clave de 256 bits — mejor portabilidad documentada - Para hashing:
SHA-256o superior — nuncaSHA-1en código nuevo - Para firma:
ECDSAconP-256tiene buena cobertura;Ed25519requiere verificar soporte en el entorno destino antes de usarlo
¿Cómo serializás la clave?
- Usás
btoa/atob(globales en Node.js 16+, browser, edge) oTextEncoder/TextDecoder(también globales en los tres) - Evitás
Buffer.from()en código compartido
¿El build lo detecta?
- TypeScript con
"lib": ["ES2020", "DOM"]entsconfig.jsonte da los tipos deSubtleCrypto. Si faltaDOM, los tipos no resuelven - Si el módulo tiene
import from 'node:crypto', Next.js lo va a advertir en build para edge routes — prestale atención
Lo que no podés concluir sin medir
Esto importa: la documentación oficial de MDN, Node.js y Next.js describe la superficie de API. Lo que no describe es rendimiento comparativo entre entornos, ni cuál tiene mejor throughput para operaciones específicas de cifrado.
Si necesitás esos números para una decisión de arquitectura —por ejemplo, si vale la pena mover un worker de cifrado al edge en lugar de a un API route con Node.js runtime— eso requiere un benchmark propio con las condiciones de carga real. Yo no tengo esos números públicos disponibles. Nadie debería comprarte ese claim sin mostrar los datos.
Lo que sí podés concluir de las fuentes oficiales: la API de superficie es compatible para AES-GCM y SHA-256 en los tres entornos. Las diferencias de soporte de algoritmos menos comunes (curvas Ed25519, por ejemplo) están documentadas y son verificables hoy mismo.
FAQ: Web Crypto API entre entornos
¿crypto.subtle está disponible globalmente en Node.js 20 sin ningún import?
Sí. Desde Node.js 19, globalThis.crypto es estable y no requiere import. En Node.js 18 LTS, está disponible pero aún era experimental para algunos algoritmos. Verificá contra los release notes de Node.js para el algoritmo específico que necesitás.
¿Puedo usar node:crypto en un Server Component de Next.js?
Sí, siempre que ese Server Component no corra en edge runtime. Los Server Components por defecto usan Node.js runtime, donde node:crypto está disponible. El conflicto aparece si movés ese componente o ese módulo al edge.
¿Cómo sé si mi Route Handler corre en edge o en Node.js?
Si no declarás export const runtime = 'edge' en el archivo, por defecto corre en Node.js runtime. Si lo declarás, corre en edge y tenés que respetar el subconjunto de APIs documentado en Next.js Edge Runtime.
¿AES-CBC también es portable entre los tres entornos?
Según MDN, AES-CBC es parte de la especificación Web Crypto. Pero AES-GCM es preferible porque incluye autenticación del cifrado (AEAD) y protección contra manipulación del ciphertext. Si ya tenés código con AES-CBC, va a funcionar en los tres entornos, pero no es la elección recomendada para código nuevo.
¿Por qué TypeScript no me avisa cuando uso APIs que no existen en edge?
Porque TypeScript tipea contra la configuración de lib en el tsconfig.json, no contra el entorno de runtime real. Si configurás "lib": ["ES2020", "DOM"], los tipos de SubtleCrypto resuelven correctamente aunque el código corra en edge. El error aparece en runtime, no en compilación. Para esto, la revisión manual del checklist de entorno importa más que los tipos.
¿Puedo compartir código criptográfico entre un módulo de React y un Middleware de Next.js sin romper nada?
Sí, si ese módulo usa exclusivamente Web Crypto API (crypto.subtle, crypto.getRandomValues) y evita cualquier import de node:crypto o Buffer. La prueba más rápida: si el módulo compila sin errores con "target": "edge" en Next.js, está en el camino correcto.
Conclusión: la API es una, los entornos son tres, el contrato es explícito
No necesitás desconfiar de Web Crypto API para usarla bien. Lo que necesitás es leer la documentación de cada entorno antes de escribir el primer módulo compartido —no después de que el deploy del Middleware explote a las 2am.
Mi postura concreta: en proyectos Next.js que cruzan server, edge y cliente, creo módulos de cifrado separados o verifico el checklist de algoritmos y APIs antes de cualquier refactor de "unifiquemos esto". El costo de una función por entorno es mínimo comparado con depurar un error de runtime en edge que el TypeScript no detectó.
Lo que no compro: la idea de que "es todo el mismo estándar, no hay nada que verificar". El estándar define la interfaz. Los entornos definen qué implementan de ese estándar. Son cosas distintas.
Si estás trabajando en Next.js Middleware con lógica de autorización que toca cifrado, el post sobre patrones de autorización en Next.js 16 Middleware tiene contexto complementario útil. Y si el módulo compartido es parte de una codebase más grande con TypeScript strict, el post sobre strict mode en tsconfig puede ahorrarte alguna sorpresa adicional.
El próximo paso práctico: abrí el tsconfig.json del proyecto, verificá la configuración de lib, y buscá cualquier import from 'node:crypto' en módulos que puedan cruzar al edge. Eso solo ya te dice si tenés deuda acá.
Fuentes originales:
Artículos Relacionados
pnpm vs npm vs yarn vs bun: la comparativa definitiva que nadie te va a dar en 2025
Usé los cuatro en proyectos reales. Uno me rompió un monorepo a las 3am. Otro me salvó la vida en producción. Te cuento todo sin filtros.
React 19 Server Components y caching: el modelo mental que me faltaba después de leer la documentación
No es otro tutorial de RSC. Es el mapa conceptual que construí después de leer la doc oficial y entender por qué el folklore sobre 'siempre usar use client' es incorrecto — y qué pasa cuando ponés Server Components en un layout real con datos dinámicos.
Cline en VS Code: lo usé dos semanas en un proyecto TypeScript y esto sobrevivió
Dos semanas usando Cline como agente autónomo de coding en un proyecto TypeScript. Qué tareas delegué, dónde se equivocó, cómo se compara con Claude Code y qué workflows no le daría nunca. Análisis con criterio de arquitectura, no de hype.
Comentarios (0)
¿Qué pensás de esto?
Dejá tu comentario en 10 segundos.
Usamos tu login solo para mostrar tu nombre y avatar. Nada de spam.