TypeScript en Node.js es de esas cosas que parecen un overhead hasta que has desplegado algo sin él y pasado una tarde debugueando un cannot read property of undefined que el compilador habría atrapado a costo cero.
Esta guía configura TypeScript en un proyecto Express desde cero — no solo el boilerplate, sino el razonamiento detrás de cada decisión.
Inicializar el proyecto
mkdir ts-node-app
cd ts-node-app
npm init -y
Instalar dependencias
TypeScript y sus definiciones de tipos son dependencias de desarrollo. El output compilado es JavaScript plano, por lo que ninguna de las herramientas de TypeScript llega a producción.
# Runtime
npm install express
# Solo desarrollo
npm install -D typescript ts-node @types/node @types/express
typescript— el compiladorts-node— ejecuta archivos.tsdirectamente sin paso de build, útil para desarrollo@types/nodey@types/express— definiciones de tipos para las APIs de Node y Express
Configurar TypeScript
Genera un tsconfig.json:
npx tsc --init
Luego configúralo para un target de Node.js:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "build"]
}
Opciones clave:
strict: true— activa todas las verificaciones estrictas. Este es el punto de TypeScript; no lo desactives.outDir: "./build"— el JS compilado va aquí, nunca lo subas al repositorioesModuleInterop: true— permiteimport express from 'express'en lugar deimport * as express from 'express'
Estructura del proyecto
ts-node-app/
├── src/
│ ├── app.ts ← configuración de Express
│ └── server.ts ← punto de entrada, inicia el servidor
├── build/ ← output compilado (en .gitignore)
├── tsconfig.json
└── package.json
Escribir la aplicación Express
// src/app.ts
import express, { Application, Request, Response, NextFunction } from 'express';
const app: Application = express();
app.use(express.json());
app.get('/', (req: Request, res: Response) => {
res.json({ status: 'ok', mensaje: 'Hola desde TypeScript.' });
});
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: err.message });
});
export default app;
// src/server.ts
import app from './app';
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => {
console.log(`Servidor corriendo en el puerto ${PORT}`);
});
Separar app de server hace que la aplicación sea importable en los tests sin vincularla a un puerto.
Agregar scripts de build
{
"scripts": {
"dev": "ts-node src/server.ts",
"build": "tsc --project ./",
"start": "node ./build/server.js"
}
}
npm run dev— desarrollo, sin paso de compilación, feedback instantáneonpm run build— compila TypeScript a./buildnpm start— ejecuta el output compilado en producción
Handlers de rutas tipados
El valor real aparece cuando empiezas a definir tipos para request y response:
// src/routes/usuarios.ts
import { Router, Request, Response } from 'express';
interface CrearUsuarioBody {
nombre: string;
email: string;
}
interface UsuarioParams {
id: string;
}
const router = Router();
router.post('/', (req: Request<{}, {}, CrearUsuarioBody>, res: Response) => {
const { nombre, email } = req.body; // completamente tipado — sin any
res.status(201).json({ id: '1', nombre, email });
});
router.get('/:id', (req: Request<UsuarioParams>, res: Response) => {
const { id } = req.params; // string, garantizado
res.json({ id, nombre: 'Esteban', email: 'e@example.com' });
});
export default router;
El genérico Request<Params, ResBody, ReqBody, Query> te da cobertura de tipos completa a lo largo de todo el ciclo de vida de la solicitud.
Variables de entorno con seguridad de tipos
// src/config.ts
function requireEnv(key: string): string {
const value = process.env[key];
if (!value) throw new Error(`Variable de entorno requerida: ${key}`);
return value;
}
export const config = {
port: parseInt(process.env.PORT ?? '3000', 10),
dbUrl: requireEnv('DATABASE_URL'),
nodeEnv: (process.env.NODE_ENV ?? 'development') as 'development' | 'production' | 'test',
};
requireEnv falla rápido al inicio en lugar de fallar en runtime cuando realmente se usa la variable.
Agregar .gitignore
node_modules/
build/
.env
La carpeta build/ es generada — nunca la subas al repositorio.
Conclusiones
- Las dependencias de TypeScript (
typescript,ts-node,@types/*) sondevDependencies— no llegan a producción strict: truees el objetivo principal. Desactivarlo es básicamente escribir JavaScript con pasos extra- Separa
app.tsdeserver.ts— hace los tests significativamente más limpios - Los genéricos
Request<Params, ResBody, ReqBody, Query>son cómo obtienes seguridad de tipos en los handlers de Express ts-nodeen desarrollo, JavaScript compilado en producción — nunca ejecutests-nodeen un entorno productivo
Una vez que has tipado un servicio Express en producción, volver a Node sin tipos se siente como trabajar sin autocompletado. El costo de configuración son unos 15 minutos; el beneficio se acumula en cada refactor.