expresslanets es la plantilla de API REST con Express y TypeScript que empezó como una serie de diez entradas en 2022 y que desde entonces ha ido incorporando mejoras, como la documentación con OpenAPI/Swagger. En esta entrada aplico el razonamiento de la entrada anterior de esta serie a este proyecto en concreto: paso la plantilla a ES Modules, sustituyo ts-node/Nodemon por tsx, y añado Vitest para poder validar tipos y comportamiento en desarrollo sin depender solo de Sonar.
Al ser una aplicación, no una librería que otros instalan, no hay consumidores externos a los que romper. Eso simplifica mucho la decisión: no necesitamos un paquete dual, solo migrar limpiamente a ESM.
Punto de partida
La configuración actual de expresslanets es esta:
// tsconfig.json (antes)
{
"compilerOptions": {
"target": "es2016",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "commonjs",
"rootDir": "./src",
"resolveJsonModule": true,
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"strictPropertyInitialization": false,
"skipLibCheck": true
}
}// package.json (antes, extracto)
"scripts": {
"build": "npx tsc",
"dev": "npx nodemon",
"format": "npx prettier --parser typescript --write ./src",
"lint": "npx eslint ./src --ext .ts"
},
"devDependencies": {
"nodemon": "^3.1.10",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
// ...
}Dos detalles importantes de este proyecto: usa TypeORM, que depende de decoradores (experimentalDecorators + emitDecoratorMetadata), y no tenía ningún test runner configurado, ni Jest ni Vitest, solo Sonar para análisis estático. Eso significa que este cambio, además de la migración a ESM, es también la primera vez que el proyecto tiene tests de verdad.

Sustituir ts-node y Nodemon por tsx
npm uninstall ts-node nodemon npm install --save-dev tsx
tsx incluye su propio modo watch, así que no hace falta Nodemon por separado:
"scripts": {
"dev": "tsx watch src/index.ts"
}Activar ES Modules en package.json
{
"type": "module"
}Actualizar tsconfig.json
Con "type": "module" activo, tsc debe generar salida ESM y resolver los módulos con las reglas de Node para ESM (NodeNext). Mantenemos los ajustes de decoradores que necesita TypeORM:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"rootDir": "./src",
"resolveJsonModule": true,
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"strictPropertyInitialization": false,
"skipLibCheck": true
}
}Un detalle que suele pillar a contrapié la primera vez: bajo moduleResolution: "NodeNext", las importaciones relativas entre tus propios archivos .ts necesitan la extensión .js explícita ,aunque el archivo origen sea .ts, porque así es como Node resuelve módulos ESM en tiempo de ejecución:
// antes
import { UserController } from './controllers/user';
// después
import { UserController } from './controllers/user.js';No perder el chequeo de tipos en desarrollo
Como comentamos al principio de esta serie, tsx no valida tipos ,solo transpila, y Sonar tampoco sustituye a tsc en eso. La solución es correr el chequeo de tipos como proceso aparte, en paralelo:
npm install --save-dev concurrently
"scripts": {
"dev": "concurrently \"tsx watch src/index.ts\" \"tsc --noEmit --watch\"",
"typecheck": "tsc --noEmit"
}Así tienes errores de tipos en tiempo real en una terminal mientras tsx se encarga de ejecutar rápido en la otra, y typecheck queda disponible como paso de verificación en CI o en un hook de pre-commit.
Añadir Vitest
npm install --save-dev vitest @vitest/coverage-v8
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['src/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
},
},
});"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"coverage": "vitest run --coverage"
}Un primer test sencillo, por ejemplo sobre un controlador o una utilidad del proyecto, ya te sirve para comprobar que Vitest arranca correctamente sobre ESM + TypeORM sin configuración adicional, algo que con Jest habría exigido activar flags experimentales de Node.
Verificar la compatibilidad con TypeORM
Tanto tsx como Vitest usan esbuild por debajo, y esbuild soporta los decoradores “legacy” (experimentalDecorators + emitDecoratorMetadata) que usa TypeORM. Aun así, conviene comprobarlo con una entidad real antes de dar la migración por cerrada: crea o edita una entidad con un par de columnas y una relación, y confirma que las metadatas de tipos se generan correctamente al arrancar con npm run dev y al ejecutar sus tests con Vitest.
Conclusiones
Con estos cambios, expresslanets pasa a ser ES Modules sin ningún consumidor externo al que romper, gana un ciclo de desarrollo más rápido con tsx, mantiene el chequeo de tipos gracias a tsc --noEmit --watch en paralelo, y estrena su primera suite de tests con Vitest. En la siguiente entrada de la serie aplico el mismo razonamiento, pero con un resultado bastante distinto, a tslane, donde al ser una librería la solución pasa por un paquete dual CommonJS/ESM en lugar de una migración directa.
Nota: La imagen de este artículo fue generada utilizando un modelo de inteligencia artificial.
Deja una respuesta