Skip to content CHRNX
Color theme Following system theme

req.user no existe, tú nomás rezas

Code reading const user = req.user above the title I Built a Framework on Express. Then I Had to Rebuild It From Scratch, on green-tea's dark graph-pattern background.

Llevo años escribiendo esta línea:

const user = req.user;

Compila. TypeScript contento. Y es una mentira con tipo.

Esa propiedad existe nomás si algún middleware corrió antes. ¿Cuál? El que alguien metió en un array, tres archivos arriba, en un orden que nadie escribió en ningún lado. Muevan ese array y tu handler se cae. No en el build. No en los tests. Se cae en prod, un martes, a las tres de la mañana, y el compa que lo movió ya está dormido.

Eso no es tipado. Es rezar con autocompletado.

Ya sé lo que vas a decir

Ay sí, mucho framework nuevo. Otro cabrón que se aburrió de Express y ahora quiere que aprendas sus decoradores. Ya hay doce. Nadie pidió el trece.

Tienes razón en la mitad. La otra mitad es que yo ya había hecho ese error, y se llamaba Expressive Tea.

Lo que maté

Expressive Tea era decoradores más InversifyJS más etapas de arranque, todo montado encima de Express. Funcionaba. Lo usé en cosas reales. Pero cargaba con todo lo que Express arrastra, y para inyectar dependencias tenías que casarte con un contenedor que pesaba más que el problema que resolvía.

Green Tea no es la versión 2. Es la misma idea después de tirar Express y tirar Inversify. Lo que queda de dependencia es reflect-metadata y ya.

Y el cambio de fondo: el centro ya no es la cadena de middlewares. Es el grafo.

Declaras qué necesitas, no cuándo corres

Cada paso dice qué necesita y qué produce. El framework saca el orden solo, revisa el cableado antes de levantar el servidor, y si algo no cuadra no arranca.

@Provider({ provides: 'db' })
class Database {
  provide() {
    const users = { u1: { id: 'u1', name: 'Diego' } };
    return { db: { find: (token: string) => users[token] ?? null } };
  }
}

@Step({ provides: 'user', needs: ['db', 'req'] })
class Authenticate {
  run(ctx) {
    const user = ctx.db.find(ctx.req.headers['x-token']);
    if (!user) throw new Unauthorized('invalid token');
    return { user };
  }
}

@Route('/users')
class UserController {
  @Get('/:id')
  @Transformer(JsonTransformer)
  getUser(@needs('user') user, @param('id') id) {
    return { requested: id, you: user };
  }
}

La firma del handler es el contrato. @needs('user') no es decoración, es la condición para que ese código exista. Si nada produce user, no compila. No te enteras en prod. Te enteras cuando guardas el archivo.

Y como el orden sale de las dependencias y no de dónde pusiste la línea, las rutas públicas no corren el paso de autenticación. No porque le hayas puesto un if. Porque no lo necesitan y el grafo lo sabe.

Poder verlo

Esta es la parte que a mí me arregló el día. app.explain('/users/:id') te imprime la cadena completa, en orden, tal como va a correr. GET /__graph__ te lo dibuja vivo. app.openapi() te saca el spec 3.1 sin que anotes nada aparte.

Diez años explicándole a la gente el orden de los middlewares dibujando flechitas en una servilleta. Ahora se lo pregunto al programa.

El tiempo real no es un plugin

Un handler que devuelve varios valores a lo largo del tiempo ya es un stream. No hay que inventarle nada. Es la misma forma que devolverías de cualquier función.

  • @Sse para mandar del servidor al cliente
  • @Ws para los dos lados
  • @Stream cuando te da igual y que el cliente escoja

Los tres son AsyncIterable. El framing, el backpressure y la limpieza los pone la casa. Tú escribes un generador y te vas.

Corre donde sea

Node, Deno, Bun y Workers. Mismo import, mismo código. Lo único que cambia es la línea de arranque: app.listen, serveDeno, serveBun o edgeHandler. HTTP, SSE y WebSocket se comportan igual en los cuatro, que es más de lo que puedo decir de cualquier cosa que haya portado a Workers en mi vida.

La seguridad viene prendida, no como opción que se te olvida: TLS y WSS, nosniff, X-Frame-Options, HSTS cuando hay https, CORS con preflight vigilado, y límites de tamaño para que no te tiren el server mandándote un JSON de dos gigas.

La parte donde no te vendo nada

Está en beta. Versionado por calendario, YY.M.PATCH-beta.N. Va camino a RC, no está ahí.

Mesh, que es inyección de dependencias entre servicios, donde @needs('billing') te resuelve igual si billing está aquí o en otra máquina, está en alpha y detrás de experimental: true. No mames, dice alfa. Ya si lo metes en producción es tu pedo.

No tiene estrellas, ni comunidad, ni un Discord con roles de colores. Tiene commits, y los tiene porque alguien lleva meses metiéndole en vez de dormir. Si venías buscando adopción masiva, te equivocaste de repo.

npm install @green-tea/core@beta reflect-metadata

No te va a hacer más rápido. No hace nada que no puedas hacer a mano con suficiente disciplina y un documento que nadie va a leer. Lo que hace es que el error salga cuando arranca el proceso y no cuando te habla el cliente.

Yo ya no quiero seguir rezándole a req.user. Por eso lo escribí.

Si le quieres entrar más macizo, la versión larga la escribí en inglés, con el desglose completo y video: I Built a Framework on Express. Then I Had to Rebuild It From Scratch.

El repo: github.com/Expressive-Tea/green-tea

Stay in the loop

Get new stories and episodes when they are published.

Subscribe