TypeScript con Node.js y Express

Intermedio
TypeScript
TypeScript
Actualizado: 04/05/2026

Diagrama: tutorial-typescript-nodejs-express

Configuración del proyecto

Para un proyecto Express con TypeScript, necesitas instalar las dependencias de tipos:

npm init -y
npm install express
npm install --save-dev typescript @types/node @types/express ts-node nodemon

Configuración recomendada en tsconfig.json para Node.js:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "moduleResolution": "Node",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Scripts recomendados en package.json:

{
  "scripts": {
    "dev": "nodemon --exec ts-node src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Tipando Request y Response

Express proporciona tipos genéricos para Request y Response que permiten tipar los cuerpos de petición, parámetros de ruta, query strings y el cuerpo de respuesta:

import { Request, Response, NextFunction } from "express";

// Request<Params, ResBody, ReqBody, Query>
interface ParamsUsuario {
    id: string;
}

interface BodyActualizarUsuario {
    nombre?: string;
    email?: string;
}

interface QueryBusqueda {
    page?: string;
    limit?: string;
    buscar?: string;
}

// Ruta con todos los genéricos tipados
router.put(
    "/usuarios/:id",
    async (
        req: Request<ParamsUsuario, unknown, BodyActualizarUsuario, QueryBusqueda>,
        res: Response,
        next: NextFunction
    ): Promise<void> => {
        const { id } = req.params; // string
        const { nombre, email } = req.body; // tipados correctamente
        const { page } = req.query; // string | undefined

        // Lógica del controlador
        res.json({ id, nombre, email });
    }
);

Estructura en capas: router, controlador, servicio

La arquitectura típica de una API Express en TypeScript separa las responsabilidades en capas:

Modelos e interfaces

// src/modelos/usuario.ts
export interface Usuario {
    id: string;
    nombre: string;
    email: string;
    rol: "admin" | "usuario";
    creadoEn: Date;
}

export interface CrearUsuarioDto {
    nombre: string;
    email: string;
    contrasena: string;
}

export interface ActualizarUsuarioDto {
    nombre?: string;
    email?: string;
}

Capa de servicio

// src/servicios/usuario.servicio.ts
import { Usuario, CrearUsuarioDto, ActualizarUsuarioDto } from "../modelos/usuario";

export class UsuarioServicio {
    private usuarios: Map<string, Usuario> = new Map();

    async crear(dto: CrearUsuarioDto): Promise<Usuario> {
        const usuario: Usuario = {
            id: crypto.randomUUID(),
            nombre: dto.nombre,
            email: dto.email,
            rol: "usuario",
            creadoEn: new Date()
        };
        this.usuarios.set(usuario.id, usuario);
        return usuario;
    }

    async obtenerPorId(id: string): Promise<Usuario | null> {
        return this.usuarios.get(id) ?? null;
    }

    async listar(): Promise<Usuario[]> {
        return Array.from(this.usuarios.values());
    }

    async actualizar(id: string, dto: ActualizarUsuarioDto): Promise<Usuario | null> {
        const usuario = this.usuarios.get(id);
        if (!usuario) return null;

        const actualizado: Usuario = { ...usuario, ...dto };
        this.usuarios.set(id, actualizado);
        return actualizado;
    }
}

Capa de controlador

// src/controladores/usuario.controlador.ts
import { Request, Response, NextFunction } from "express";
import { UsuarioServicio } from "../servicios/usuario.servicio";
import { CrearUsuarioDto } from "../modelos/usuario";

export class UsuarioControlador {
    constructor(private readonly servicio: UsuarioServicio) {}

    listar = async (_req: Request, res: Response, next: NextFunction): Promise<void> => {
        try {
            const usuarios = await this.servicio.listar();
            res.json(usuarios);
        } catch (error) {
            next(error);
        }
    };

    obtenerPorId = async (
        req: Request<{ id: string }>,
        res: Response,
        next: NextFunction
    ): Promise<void> => {
        try {
            const usuario = await this.servicio.obtenerPorId(req.params.id);
            if (!usuario) {
                res.status(404).json({ mensaje: "Usuario no encontrado" });
                return;
            }
            res.json(usuario);
        } catch (error) {
            next(error);
        }
    };

    crear = async (
        req: Request<{}, {}, CrearUsuarioDto>,
        res: Response,
        next: NextFunction
    ): Promise<void> => {
        try {
            const usuario = await this.servicio.crear(req.body);
            res.status(201).json(usuario);
        } catch (error) {
            next(error);
        }
    };
}

Router

// src/rutas/usuario.rutas.ts
import { Router } from "express";
import { UsuarioControlador } from "../controladores/usuario.controlador";
import { UsuarioServicio } from "../servicios/usuario.servicio";

const router = Router();
const servicio = new UsuarioServicio();
const controlador = new UsuarioControlador(servicio);

router.get("/", controlador.listar);
router.get("/:id", controlador.obtenerPorId);
router.post("/", controlador.crear);

export default router;

Middleware tipado

Los middlewares en Express con TypeScript siguen la misma firma tipada:

// Middleware de autenticación
import { Request, Response, NextFunction } from "express";

// Extender el tipo Request para añadir el usuario autenticado
declare global {
    namespace Express {
        interface Request {
            usuario?: { id: string; rol: "admin" | "usuario" };
        }
    }
}

function autenticar(req: Request, res: Response, next: NextFunction): void {
    const token = req.headers.authorization?.split(" ")[1];

    if (!token) {
        res.status(401).json({ mensaje: "Token requerido" });
        return;
    }

    // Verificar token (simplificado)
    try {
        req.usuario = { id: "usuario-123", rol: "usuario" };
        next();
    } catch {
        res.status(401).json({ mensaje: "Token inválido" });
    }
}

// Middleware de validación
function validarCuerpo<T>(esquema: (body: unknown) => body is T) {
    return (req: Request, res: Response, next: NextFunction): void => {
        if (!esquema(req.body)) {
            res.status(400).json({ mensaje: "Datos de entrada inválidos" });
            return;
        }
        next();
    };
}

Manejo de errores global

// src/middleware/errores.ts
import { Request, Response, NextFunction } from "express";

interface ErrorHTTP extends Error {
    statusCode?: number;
}

export function manejarErrores(
    error: ErrorHTTP,
    _req: Request,
    res: Response,
    _next: NextFunction
): void {
    const statusCode = error.statusCode ?? 500;
    const mensaje = statusCode === 500 ? "Error interno del servidor" : error.message;

    console.error(`[Error ${statusCode}] ${error.message}`);

    res.status(statusCode).json({
        error: {
            mensaje,
            ...(process.env.NODE_ENV === "development" && { pila: error.stack })
        }
    });
}

Aplicación principal

// src/index.ts
import express from "express";
import usuariosRouter from "./rutas/usuario.rutas";
import { manejarErrores } from "./middleware/errores";

const app = express();
const PUERTO = process.env.PORT ? parseInt(process.env.PORT) : 3000;

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use("/api/usuarios", usuariosRouter);

app.use(manejarErrores);

app.listen(PUERTO, () => {
    console.log(`Servidor escuchando en http://localhost:${PUERTO}`);
});

export default app;

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en TypeScript

Documentación oficial de TypeScript
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de TypeScript

Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Configurar un proyecto Node.js con TypeScript usando las opciones correctas de tsconfig
  • Tipar correctamente Request, Response y NextFunction de Express
  • Crear interfaces tipadas para el cuerpo de peticiones y parámetros de ruta
  • Implementar middleware tipado para autenticación y validación
  • Estructurar un proyecto Express en capas (router, controlador, servicio) con TypeScript