Cómo Deno 2.x resolvió silenciosamente los problemas de compatibilidad con Node y la fatiga por herramientas
Este artículo analiza las versiones 2.0 a 2.9 de Deno, mostrando cómo la compatibilidad con npm, los conjuntos de permisos y las herramientas integradas eliminaron las barreras que antes hacían que los desarrolladores lo abandonaran.
Esa prueba se realizó hace cinco años.
El mes pasado, un proyecto secundario rápido necesitaba un pequeño servidor HTTP. Un directorio nuevo, deno init, y diez minutos después ya había un servidor funcional con TypeScript, pruebas, formato y herramientas de validación listas para usar. No fue necesario escribir ningún archivo tsconfig. Ni configurar prettier. Ni eslint. Ni jest.config.ts. Tampoco había rastro de package.json.
Al verificar la versión, apareció 2.9.3.
Resulta que Deno lanzó diez versiones menores entre octubre de 2024 y julio de 2026, y cada una resolvió silenciosamente uno de los problemas exactos que causaron el abandono inicial. En ese momento nada de eso se notó porque la herramienta ya había sido clasificada mentalmente como “una idea interesante, pero no lista para uso real”, y no había motivo para volver a examinarla.
A continuación se detalla qué cambió realmente, abordando una queja antigua a la vez.
Queja 1: Los paquetes de npm eran considerados de segunda categoría
Ese fue el problema principal para muchos desarrolladores, incluido este. Bajo Deno 1.x, cualquier cosa que no estuviera publicada en deno.land/x o que careciera de URLs de módulo ES adecuadas simplemente no funcionaba. La separación entre el mundo de Deno y el mundo de npm parecía ser permanente.
Deno 2.0, lanzado en octubre de 2024, cerró completamente esa brecha. Ahora se puede cargar directamente cualquier paquete del catálogo de npm, que cuenta con más de dos millones de paquetes, utilizando el especificador npm::
import express from "npm:express@5";
import { PrismaClient } from "npm:@prisma/client";
Si prefieres seguir utilizando un package.json, también está soportado. Deno lo analiza, obtiene las dependencias del registro de npm e incluso puede generar una carpeta local node_modules si se activa esa opción. Los registros privados también funcionan a través de .npmrc, exactamente como lo hacen en Node.
La estadística que realmente lo demuestra: más del 75 por ciento del propio conjunto de pruebas de Node ahora se ejecuta con éxito en Deno. Eso no es un parche superficial; indica una compatibilidad real y verificada.
El momento decisivo fue cuando se probó Deno con un proyecto Express existente. No fue necesario cambiar ni una sola instrucción de importación. Se añadió un archivo deno.json con "nodeModulesDir": "auto", se ejecutó deno install, y las instalaciones desde cero tardaron alrededor de 900 ms, en comparación con más de 3 segundos para npm bajo las mismas condiciones (probado con un caché limpio en un proyecto que utiliza React, Vite, Babel y ESLint). El servidor funcionó correctamente desde el primer intento.
Queja 2: el sistema de permisos resultaba molesto en la práctica
El modelo de permisos tenía sentido desde el punto de vista conceptual. En la práctica, significaba tener que volver a escribir algo similar en cada ejecución:
deno run --allow-read=./data --allow-write=./data --allow-net=api.example.com --allow-env main.ts
Si se olvida una bandera, el proceso muere. Si se agrega una nueva dependencia que lee una variable de entorno, el proceso muere de nuevo. Parecía más bien una tarea de configuración de reglas de firewall que el desarrollo de un proyecto paralelo.
Deno 2.5, lanzado en septiembre de 2025, introdujo conjuntos de permisos definidos directamente en el archivo de configuración. Se declaran conjuntos con nombre bajo la clave "permissions" en deno.json, y luego se invocan con la bandera -P (abreviatura de --permission-set):
{
"permissions": {
"default": {
"read": ["./src", "./data"],
"write": ["./data"],
"net": ["api.example.com", "0.0.0.0:3000"],
"env": true
},
"test": {
"read": true,
"write": ["./tmp"],
"net": false
}
}
}
A partir de ahí, al ejecutar deno run -P main.ts se carga automáticamente el conjunto “por defecto” del archivo deno.json ubicado en el directorio de trabajo; también se puede ejecutar deno test -P=test para aplicar un conjunto separado exclusivo para pruebas. El código de prueba y el código de producción cuentan con diferentes rangos de permisos sin necesidad de modificar ni una sola opción en la línea de comandos. Las garantías de seguridad subyacentes no han cambiado; simplemente ha desaparecido la complejidad asociada a ellas.
También existe una variable de entorno DENO_AUDIT_PERMISSIONS, que genera un registro en formato JSONL que registra cada verificación de permisos realizada mientras el programa se ejecuta. Esto permite ver con exactitud a qué recursos intentan acceder las dependencias sin tener que examinar su código fuente.
Queja 3: Aún necesitaba una serie de herramientas adicionales
La ventaja y al mismo tiempo la desventaja de Node es que casi toda responsabilidad se delega a un paquete separado. ¿Necesitas formato? Usa prettier. ¿Revisión de código? ESLint y algunos plugins. ¿Pruebas? Jest o Vitest, además de un archivo de configuración para manejar las transformaciones de TypeScript. ¿Verificación de tipos? tsc, configurado de forma independiente de lo que ejecuta tu código. ¿Empaquetado? Elige entre media docena de herramientas de empaquetado, cada una con su propio dialecto de configuración.
Deno integra todo esto como subcomandos integrados. Estos existían ya antes del lanzamiento 2.0:
deno fmt # formats TS, JS, JSON, HTML, CSS, YAML, SQL
deno lint # built-in linter with quick fixes
deno test # test runner with coverage, snapshots, sharding
deno check # type checking
deno compile # single binary, cross-platform, code signing
deno bench # benchmarking
deno doc # documentation generation
A partir de la versión 2.8, se añadieron seis subcomandos más:
deno audit # security audit of dependencies
deno audit fix # auto-upgrade vulnerable packages to nearest patched version
deno why # explain why a package is installed (traces dependency paths)
deno transpile # strip types, emit .d.ts declarations
deno pack # build an npm-publishable tarball from JSR/Deno code
deno ci # reproducible install for CI (errors without lockfile)
Y con la versión 2.9 llegaron aún más:
deno desktop # build native desktop apps via webview (experimental)
deno list # show dependency tree (like npm ls)
deno link/unlink # local package linking for development
El que se utiliza con más frecuencia es deno compile. Al crear una pequeña herramienta de línea de comandos y ejecutar deno compile --target x86_64-unknown-linux-gnu main.ts, se genera un binario autónomo. No es necesario instalar nada más en la máquina destino; basta con copiarlo a un servidor y ejecutarlo.
Cuidado: el binario resultante incluye a V8 y todo el entorno de ejecución de Deno, por lo que las aplicaciones típicas ocupan entre 60 y 100 MB. Cuando el tamaño es un problema,
deno compile --bundle(aún inestable en la versión 2.8) aplica técnicas de optimización agresivas que pueden reducir significativamente el tamaño de los scripts simples. El propio blog de Deno demostró que un ejemplo con lodash “hello world” ocupaba 1,5 MB al utilizar--bundle --minify.
Queja 4: migrar un proyecto real parecía algo inalcanzable
El mayor obstáculo para cambiar los entornos de ejecución suele no tener nada que ver con el propio entorno. Se trata del archivo de bloqueo, la estructura de node_modules de la que dependen las herramientas existentes, y las llamadas dispersas a require() en todo el código.
Deno 2.9 introdujo la funcionalidad de inicialización del archivo de bloqueo. Supongamos que un proyecto ya registra sus dependencias a través de uno de los archivos de bloqueo comunes de los gestores de paquetes, como package-lock.json de npm, pnpm-lock.yaml, yarn.lock o bun.lock, pero nunca ha generado un deno.lock. Al ejecutar deno install en ese proyecto, se crea el archivo deno.lock faltante directamente a partir del archivo que se encuentre. Las versiones resueltas coinciden y los hashes de integridad también; no hay riesgo de desviaciones en la resolución.
En situaciones que realmente necesitan un directorio físico node_modules —como complementos nativos o herramientas que escanean directamente el sistema de archivos—, establecer "nodeModulesDir": "auto" indica a Deno que cree uno. También existe una opción de layout elevado ("nodeModulesLinker": "hoisted" en deno.json, disponible desde la versión 2.8) para herramientas más antiguas diseñadas con la estructura de directorios plana de npm en lugar del enlazado simbólico al estilo pnpm.
El complemento de nodo es una solución muy práctica: Deno instala automáticamente un binario sustituto de node en tu PATH, sin necesidad de configuración manual, en el mismo momento en que se instala Deno. Mientras no haya otro programa que ya proporcione un ejecutable de node, este complemento intercepta las llamadas a la CLI de Node, traduce los argumentos y los pasa a Deno. Eso significa que los scripts de CI que siguen utilizando node dist/server.js seguirán funcionando sin necesidad de realizar modificaciones. Puedes desactivar este comportamiento con DENO_DISABLE_NODE_SHIM=1 si prefieres que no ocurra automáticamente.
Las importaciones con especificador simple —escribir import fs from "fs" para que se resuelva como node:fs— se incluyeron en la versión 2.0 y se volvieron completamente estables, funcionando sin necesidad de flags ni configuración alguna a partir de la versión 2.9. No es necesario volver atrás y reescribir las instrucciones de importación para realizar la migración.
El intento de migración
Considere un proyecto modesto de Hono API (Hono es un framework HTTP ligero comparable a Express) con aproximadamente 15 rutas, una base de datos Postgres a la que se accede mediante Drizzle ORM, y un procesador de tareas en segundo plano. Había estado funcionando en Node 22 con pnpm. Todo el experimento se realizó con Deno 2.9.3.
Así fue la conversión:
- Ejecute
deno installdesde el directorio raíz del proyecto. Genera un archivodeno.lockdirectamente a partir del existentepnpm-lock.yamlen menos de dos segundos. - Agregue
"nodeModulesDir": "auto"dentro de un archivodeno.jsonrecién creado. - Inicie la aplicación con
deno run -A src/server.ts.
Funciona sin problemas. Cada una de las 15 rutas responde correctamente, las consultas basadas en Drizzle se ejecutan como se esperaba, y el trabajador de cola sigue procesando tareas en segundo plano.
No obstante, tres cosas fallaron:
- Un archivo de prueba dependía de
jest.mock(), que no tiene equivalente en el ejecutor de pruebas integrado de Deno. Sustituirlo por un mock manual toma menos de cinco minutos. - Una dependencia hacía referencia a
__dirnamedentro de un archivo CommonJS, pero Deno lo había cargado como módulo ES. La solución fue agregar"type": "commonjs"a la configuración local de ese paquete específico.
process.env.NODE_ENV sin importar explícitamente process. Esa parte funcionó bien, ya que Deno ha expuesto process como global desde la versión 2.0; sin embargo, las banderas de permisos no incluían env: true para ese script en particular, por lo que fue necesario agregarlo.Toda la conversión tomó aproximadamente 25 minutos desde el inicio hasta el final. Los tiempos de arranque cayeron de unos 620 ms a 320 ms, según mediciones realizadas con hyperfine en 50 ejecuciones. El uso de memoria en estado inactivo (RSS) disminuyó de 142 MB a 64 MB. Además, se pudieron eliminar cuatro archivos de configuración separados: prettier, eslint, jest y tsconfig.
Aspectos aún imperfectos
El título de este artículo menciona “todo lo que odiaba”, y las quejas enumeradas anteriormente son reales, quejas comunes entre los desarrolladores de Node. Dicho esto, hay algunas consideraciones más recientes que vale la pena señalar:
- La cobertura de la API de Node no es completa. Una tasa de aprobación del 75 por ciento en el conjunto de pruebas de Node implica que una cuarta parte sigue fallando. Esa brecha podría no afectar a una carga de trabajo típica, pero quienes dependan de aspectos poco comunes de
node:vm, del uso programático denode:inspectoro de funcionalidades más avanzadas denode:clusterdeberían consultar primero el panel de compatibilidad en node-test-viewer.deno.dev.
node_modules. Cualquier componente con enlaces a C++ — como sharp, bcrypt, sqlite3 y paquetes similares — requiere tanto la configuración nodeModulesDir como el flag --allow-ffi. Funciona, pero supone un paso adicional de configuración que es fácil pasar por alto accidentalmente.postinstall — como los pasos de compilación con node-gyp o prisma generate — necesitan un permiso explícito mediante --allow-scripts=npm:nombre-del-paquete. Se trata de una decisión de seguridad intencionada, pero puede sorprender al equipo en medio de la migración.process.versions.node y modifican su comportamiento en consecuencia, o incluyen de forma fija rutas de archivo como /node_modules/.cache. La estructura de módulos basada en enlaces simbólicos al estilo pnpm de Deno genera problemas en algunas de estas herramientas. Existe una opción de modo elevado para solucionar esto, pero se trata solo de un parche y no de una solución real.Ninguno de estos problemas fue lo suficientemente grave como para obstaculizar la migración descrita anteriormente. Podrían serlo en otros proyectos, por lo que vale la pena verificarlos de antemano en lugar de descubrirlos durante la migración.
Por qué estas soluciones pasaron desapercibidas
Hubo versiones menores lanzadas a lo largo de 21 meses, cada una resolviendo silenciosamente de tres a cinco problemas. No hubo una reescritura drástica ni un evento de lanzamiento tipo “Deno 3.0”. El equipo implementó conjuntos de permisos detallados en la versión 2.5, logró velocidades de instalación al nivel de npm en la 2.8 y añadió la función de inicialización de lockfile en la 2.9, tratando cada uno de estos avances como mantenimiento rutinario y no como noticias destacadas.
En contraste, así es como normalmente se da a conocer la transición de un framework: con un artículo de blog, una presentación en conferencia, una guía de migración, una ola de comentarios en redes sociales, opiniones contundentes y respuestas a ellas. Deno evitó todo ese ciclo de debate público y simplemente distribuyó las correcciones directamente.
Los desarrolladores que descartaron rápidamente este entorno de ejecución lo hicieron porque formaron una opinión una vez y nunca la revisaron a medida que las herramientas maduraban. Eso es una falta de atención, no un defecto del proyecto en sí.
Para quienes tuvieron su primera impresión de Deno antes de la versión 2.0, la herramienta probada en aquel entonces prácticamente ha desaparecido. Lo que existe hoy en día se parece menos a “un entorno de ejecución de TypeScript interesante que no puede interactuar con npm” y más a “Node sin su carga adicional”. Las frustraciones de aquel entonces eran legítimas; desde entonces se han resuelto. Vale la pena echarle otro vistazo.
Lecturas relacionadas
- Sustituyendo Jest con el ejecutor de pruebas nativo de Node en Node 24 — Una migración real muestra cómo el ejecutor de pruebas integrado en Node 24 y el soporte nativo para TypeScript reducen el tiempo de los procesos CI al mismo tiempo que eliminan cuatro dependencias.