APIs con Node, TypeScript, Koa y TypeORM

En mi (parsimoniosa) búsqueda de plataformas alternativas a .NET más de una vez me he llegado a plantear el uso de NodeJS + TypeScript. A fin de cuentas NodeJS es multiplataforma, ya tiene cierta madurez y es supuestamente estable. Con TypeScript llevo ya un par de años y, pese a sus «cosillas», estoy razonablemente contento con él.

Por eso cuando hace unos días contactó conmigo Javier Avilés para hablarme sobre un repositorio que ha montado como plantilla para crear APIs con Node, TypeScript, Koa y TypeORM me pareció interesante echarle un vistazo.

OJO: Si has llegado aquí buscando un tutorial sobre cómo crear APIs con Node y TypeScript, mejor léete directamente la documentación de la plantilla creada por Javier. Ahí encontrarás información más útil que en este post. Aquí me limitaré a hacer un repaso somero a lo que me ha parecido la plantilla.

Las acciones web

De NodeJS no hay mucho que contar a estas alturas, y sobre TypeScript ya he escrito suficiente, por lo que intentaré centrarme en otras cosas. Puesto que el proyecto es para montar un API Web, parece razonable empezar por ver qué tipo de servidor web y framework se está usando.

Como framework para crear el API se está usando koa. Sin estar muy metido en node, diría que es una de las librerías/frameworks más populares para servidores web junto con express (del que incluso escribí un tutorial cuando hacía ese tipo de cosas). Conocía koa un poco por sus primeras versiones y recuerdo que hacía un uso llamativo de los generadores para huir del callback hell. En las versiones actuales se aprovecha async/await para conseguir dar un aspecto líneal al código asíncrono haciéndolo bastante claro y legible.

Al igual que la mayoría de frameworks web de lenguajes que no viven encorsetados en clases, la gestión de peticiones y respuestas se realiza mediante funciones que reciben la petición y generan la respuesta, en este caso ambas empaquetas en un objeto context. Lo bueno de este enfoque es que evita código repetitivo y ayuda a que todo sea más fácil de componer:

app.use(async ctx => {
  ctx.body = 'Hello World';
});

Es un estilo que me gustó mucho cuando conocí node, y más aún cuando jugué con él en clojure. Lo cierto es que la filosofía de la web, basada en petición/respuesta, se ajusta muy bien a ser modelada con funciones en lugar de con clases. Además, eso hace que todo sea muy homogéneo, ya conceptos típicos de frameworks webs como middlewares se reducen a crear decoradores sobre funciones.

Precisamente esta es una de las cosas que no me convencen de la plantilla que estamos analizando: el uso de clases estáticas para agrupar estas funciones que sirven de manejadores de peticiones:

export default class UserController {
  public static async getUsers (ctx: BaseContext) {
    // ...
  }
}

El uso de una clase para encapsular en método estático no aporta nada (a menos que me esté perdido algo) y en lenguajes como TypeScript lo consideraría un code smell.

Las rutas

En cualquier aplicación web acaba siendo necesario gestionar rutas, y en este caso se está usando koa-router. No tengo ni idea de si hay más routers para koa o de lo bueno o malo que es éste, pero parece fácil de manejar con el típico API basado en métodos con los nombres de los verbos:

var app = new Koa();
var router = new Router();

router.get('/', (ctx, next) => {
  // ctx.router available
});

app
  .use(router.routes())
  .use(router.allowedMethods());

Entre las librerías que conozco hay varias opciones para gestionar el registro de rutas. Algunas se basan puramente en convenciones (al estilo RoR), otras permiten utilizar metainformación en forma de atributos o anotaciones (uno de los varios estilos soportados ASPNET MVC), y las hay que registran las rutas explícitamente.

En el caso de la plantilla se está haciendo un registro explícito de las rutas y además se hace todo en un único fichero:

// routes.ts

router.get('/users', controller.user.getUsers);
router.get('/users/:id', controller.user.getUser);
// ...

Personalmente me gusta ese estilo. Creo que es cómodo tener centralizada la definición de rutas y poder comprobar de un vistazo qué rutas existen en la aplicación y qué formato siguen. En el caso de que el número de rutas creciese mucho siempre se podría partir en varios ficheros por áreas de aplicación.

La contrapartida que tiene es que cuando añades un nuevo manejador de rutas necesitas tocar en dos sitios: el fichero donde defines la función y el fichero que contiene el registro de rutas. Para mi es un mal menor, pero no deja de ser un incordio y puede dar lugar a fallos si te olvida registrar la ruta del manejador que acabas de crear.

Una cosa que me gusta de la plantilla es que no se usa un contenedor de inversión de control. He visto otras implementaciones de este estilo que tratan de copiar demasiado los patrones de uso de plataformas como .NET y Java, y acaban replicándolas pieza a pieza con componentes que no son realmente necesarios.

Los datos

Para tratar con los datos se está usando TypeORM, una librería que se define a si misma como un ORM que soporta los patrones de Active Record y Data Mapper.

Para mi eso se queda un poco lejos de lo que espero de un ORM. Como Data Mapper estaría al nivel de micro ORMs tipo Dapper o Massive. Como Active Record… bueno, la verdad es que excepto las inspiradas en Rails, no conozco muchas librerías que hayan triunfado con ese modelo.

En muchos casos la mejor opción es usar un Data Mapper que te simplifique la generación de consultas y el mapeo de resultados a objetos. Pero hay que tener en cuenta las partes que te estás perdiendo por uno usar un ORM completo, y que van mucho más allá de generar automáticamente SQL y materializar objetos: identity map, unit of work, persistencia por alcance, etc.

El uso de TypeORM parece sencillo, aunque si eres de los que piensa que tu modelo debe ignorar la persistencia, vete olvidando. Todo se basa en el uso de decoradores sobre tus entidades:

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id: number;

    @Column({
        length: 80
    })
    @Length(10, 80)
    name: string;

    @Column({
        length: 100
    })
    @Length(10, 100)
    @IsEmail()
    email: string;
}

No me apasiona la idea de contaminar mi modelo de dominio con cosas relacionadas con la base de datos, pero lo cierto es que en el momento en que decides usar TypeORM me da la sensación de que tampoco es que vayas a tener un modelo de dominio muy rico y vas a usar otro tipo de diseño, así que no es tan grave.

En la plantilla se utiliza también class-validator, una librería para incluir reglas de validación de forma declarativa en nuestras «entidades». De ahí sale por ejemplo el decorador IsEmail del ejemplo anterior.

Hace muchos años escribí en contra de mezclar la validación con las entidades y sigo pensando lo mismo. Una entidad por definición debería mantener sus invariantes y no puede encontrarse en un estado inválido. Sin embargo, por mucho que aquí las llamemos entidades no estamos trabajando más que con DTOs, por lo que tiene más sentido unirlos a las reglas de validación.

Actualización: Me explica Javier que realmente TypeORM sí que tiene persistencia por alcance (usando los decoradores correspondientes) e incluso un cierto soporte para UoW a través de transacciones.

El proyecto

El proyecto usa las herramientas típicas de node y TypeScript. Además permite usar docker para facilitar las pruebas usando una máquina ya preparada con postgreSQL.

No sabía que se podía usar la propiedad engines en el fichero package.json para fijar las versiones de node y npm que requiere el proyecto, pero me ha parecido muy útil teniendo en cuenta lo (no) estable que es todo el ecosistema de Javascript.

Para la parte de TypeScript usa ts-node, que permite ejecutar TypeScript en node compilándolo al vuelo. Lo conocía por haberlo usado con alguna librería de testing. Aun así, para la compilación de producción se está generando javascript, imagino que para evitar tener que pagar el coste de la compilación. No sé si realmente es necesario (supongo que una vez compilado queda todo en memoria y no hay que estar recompilando nada en cada petición), pero parece más limpio y evitas tener la dependencia sobre TypeScript en el entorno de producción.

El código está organizado en carpetas basadas en roles (controllers, entities, …). Nuevamente es cuestión de gustos, pero prefiero organizarlo en base a funcionalidades porque creo que es mejor tener cerca físicamente los ficheros que vas a tener que tocar a la vez.

Echo de menos algunos tests, aunque sean de ejemplo. Cuando desarrollas un API Web muchas veces no tiene mucho sentido recurrir a tests unitarios, y montar test de integración supone más esfuerzo, por lo que tener un ejemplo de cómo se podrían escribir vendría bien. Teniendo además la imagen de docker para levantar la base de datos imagino que no debería ser demasiado complicado preparar el entorno.

En resumen

Me ha gustado la plantilla que ha preparado Javier para este tipo de proyectos. Puede servir como base para desarrollar un API Web basada en TypeScript y Koa, y es lo bastante pequeña como para que pueda leerte todo el código en un rato y entenderlo sin problemas. Entre las cosas que me han gusto un poco menos está el uso (para mi innecesario) de métodos estáticos y la organización basada en roles en lugar de funcionalidades, pero son detalles menores.

Se agradece que no haya intentado meter demasiadas cosas innecesarias y que no sea una copia directa de un proyecto en Sprint o ASP.NET MVC, como pasa en otras plantillas basadas en TypeScript que tiene un aspecto demasiado enterprise/javero/nettero para mi gusto.