Skip to content

3 Middlewares

Introducción

Cuando se recibe una petición a través del protocolo HTTP, este petición debe ser tratada dentro del método que se indica. Como ya vimos en el tema anterior, cuando se realiza una petición y se envía un cuerpo del mismo, este cuerpo va llegando poco a poco a medida que se va recibiendo la información, por lo que se debería de tratar la información una vez que haya finalizado la petición, de tal forma que se vaya recolectando la información poco a poco vaya llegando en un buffer:

app.post('/pokemon', (req, res) => {
    let body = ''

    req.on('data', chunk => { //(1)!
        body += chunk.toString()
    })

    req.on('end', () => { //(2)!
        const data = JSON.parse(body)

        body = data

        //(3)!
    })
})
  1. Va obteniendo la información según va llegando, y almacenándola en la variable body.
  2. Una vez finalizada la petición parsea el buffer en JSON
  3. Una vez parseada la información a JSON se trata como necesitemos, añadiéndola a la base de datos o devolviéndola como respuesta.

Cada vez que tuviéramos que realizar una operación en la cual se requiera el cuerpo en la petición, sería necesario realizar los mismos pasos que en el ejemplo, incluso parsearlo a otro tipo de contenido si fuese necesario.

Middleware

Hasta ahora, cuando se realizaba una petición, esta petición era tratada y procesada por el marco de express, y luego devolvía una response:

Marco Express

Figura 3 - Marco Express

Un middleware se considera como un paso intermedio, entre la petición y el proceso de la misma, en la cuál se pueden realizar operaciones para cualquiera de los métodos indicados. De esta forma, si la petición requiere de un cuerpo en formato JSON, el middleware se encargaría de realizar el parseo y luego ejecutaría el proceso en el método correspondiente.

Es importante que la callback de un middleware reciba el método next(), ya que este método será el encargado de mandar la request modificada a la petición necesaria.

Middleware

Figura 3 - Marco Express

El middleware se puede usar:

  • Para cualquier método y ruta, haciendo uso del método use(cb)
  • Para cualquier método pero para una ruta específica, usando use(path, cb)
  • Para cualquier método cuya ruta que cumpla un expresión regular, use(pathToRegex, cb)
  • Para un método específico, haciendo uso del método a usar.

El middleware puede ir colocado en cualquier parte del código donde sea necesario usarlo. Aunque se recomienda que dependiendo del tipo de acceso que se va a tener, se coloque en el lugar pertinente.

app.use((req, res, next) => {
    console.log('mi primer middleware')

    if (req.method !== "POST") return next()
    if (req.headers['content-type'] !== "application/json") return next()


    let body = ''

    req.on('data', chunk => {
        body += chunk.toString()
    })

    req.on('end', () => {
        const data = JSON.parse(body)
        data.timestamp = Date.now()
        req.body = data
        next()
    })
    next() // (1)!
})

app.post('/pokemon', (req, res) => {
    const { body } = req

    // Procesar body NO ES NECESARIO PARSEAR A JSON
})
  1. ⚠️IMPORTANTE⚠️, indica que tiene que continuar a la siguiente ruta que le toca

En el ejemplo anterior, creamos un middleware que parsee el cuerpo de una petición a JSON. En este ejemplo, observamos que usamos el método use() para cualquier ruta y método. Sin embargo, en su interior solo realiza el parseo si el método es POST y el tipo de contenido indicado en el header es JSON. Es importante indicar el método next() para que localice la ruta una vez finaliza el middleware. Una vez finalizado el middleware podemos procesar la petición de una forma más sencilla, ya en formato JSON.

Podemos tener tantos middlewares como sea necesarios, y podemos usarlos en los métodos y rutas que necesitemos:

app.post((res,req, next)=>{
    if (req.headers['content-type'] !== "application/json") return next()

    let body = ''

    req.on('data', chunk => {
        body += chunk.toString()
    })

    req.on('end', () => {
        const data = JSON.parse(body)
        data.timestamp = Date.now()
        req.body = data
        next()
    })
    next()
})

En este ejemplo, hemos creado un middleware para cualquier ruta que realice una petición con método POST.

Middleware de terceros

El uso de middlewares es bastante común en el diseño de aplicaciones REST API, por lo que existe una gran gama de middleware ya creado que podemos usar sin problema. Express sin ir más lejos, tiene su propio middleware para poder parsear el cuerpo de una petición a JSON. Esto tiene la ventaja de que no es necesario realizar todo lo anterior para poder obtener el cuerpo en formato JSON.

app.use(express.json())

app.post('/pokemon', (req, res) => {
    const { body } = req

    // Procesar body NO ES NECESARIO PARSEAR A JSON
})

Seguridad en Express

Al usar Express, por defecto, se añade una cabecera x-powered-by que indica que estamos usando Express para la creación de nuestra API. Cierto es, que esto puede suponer un problema de seguridad, por lo que se recomienda su deshabilitación con el método disable. El método deshabilita cualquier cabecera que le indiquemos:

app.disable('x-powered-by')

POST vs PATCH vs PUT

La idempotencia es la propiedad de realizar una acción determinada varias veces y aún así conseguir siempre el mismo resultado que se obtendría al realizar una sola vez.

Muchas personas no saben diferenciar entre los métodos POST, PUT o PATCH, a pesar de que son diferentes entre sí. A la hora de diseñar una API REST es conveniente conocer la diferencia para poder hacer las operaciones necesarias de forma correcta.

Con el método POST creamos un nuevo elemento o recurso en el servidor, además no es necesario un identificador del elemento a crear. El método POST no es un método idempotente ya que cada vez que se ejecuta crea un elemento diferente.

El método PUT sirve para actualizar totalmente un elemento o un recurso, por lo que sería necesario tener el identificador del elemento. En caso de que el elemento no exista, lo crea.

El método PATCH sirve para realizar una actualización parcial de un elemento o recurso dado su identificador, pero en este caso si no existe el elemento no lo crea.

Tanto el método PUT como el método PATCH son elementos idempotentes ya que siempre devuelven el mismo resultado cuando se ejecuta. Sin embargo, no lo serían en caso de tener una propiedad llamada updateAt que almacene la fecha de actualización, entonces no devuelven el mismo resultado en cada ejecución.

Hay que tener en cuenta que siempre que se vaya a crear una nuevo recurso o actualizarlo, será necesario validar el cuerpo de la petición.

Validaciones

Cuando se realiza una petición de actualización o de creación, es importante validar el cuerpo de dicha petición para que concuerde con la estructura del resto de elementos de la API. Por ejemplo, si tenemos un recurso de estudiantes universitarios se debería validar que el campo edad sea un campo numérico positivo mayores que 18.

app.post('/students', (req, res) => {
    const { age } = req.body

    if(!age || typeof age !== "number" && age < 18){
        res.status(400).json({message: ""})    
    }
})

Realizar estas validaciones puede ser algo pesado e incluso repetitivo. Hay que tener en cuenta que estos problemas no se solucionarán haciendo uso de Typescript.

Existen diferentes librerías que te facilitan dicha tarea. Zod es una de las librerías más conocidas para dicha tarea:

const z = require('zod')

/*(1)!*/const movieSchema = z.object({ //(2)!
    title: z.string({ // (3)!
        invalid_type_error: 'Movie title must be a string',
        required_error: 'Movie title is required'
    }),
    year: z.number().int().min(1900).max(2024), //(4)!
    director: z.string(), // (5)!
    duration: z.number().int().positive(), //(6)!
    rate: z.number().int().min(0).max(10).default(0), //(7)!
    poster: z.string().url({ //(8)!
        message: 'Poster must be a valid'
    }),
    genres: z.enum(['Action', 'Adventure', 'Comedy', 'Drama', 'Fantasy', 'Horror', 'Thriller', 'Sci-Fi', 'Crime']).array({
        required_error: 'Movie genre is required',
        invalid_type_error: 'Movie genre must be an array of enum Genre'
    }) //(9)!
})

function validateMovie(object) {
    return movieSchema.safeParse(object)
}

app.post('/students', (req, res) => {
    const result = validateMovie(req.body) //(10)!

    if (result.error) { //(11)!
        return res.status(400).json({ error: JSON.parse(result.error.message) })
    }
})
  1. Esquema donde se indicará que tipo de elemento se espera.
  2. Se indica que el elemento será de tipo object.
  3. Se indica que el titulo debe de ser una cadena. Además se personaliza los mensajes en caso de que haya un error de tipo o un error de requerimiento.
  4. Se indica que el año ha de ser un número positivo comprendido entre 1900 y 2024.
  5. Se indica que el directo debe ser una cadena.
  6. Se indica que la duración debe ser un número positivo.
  7. Se indica que la calificación debe ser un número positivo comprendido entre 0 y 10, y en caso de que no se indique su valor por defecto será 0.
  8. Se indica que el poster debe ser una cadena con URL y se personaliza el mensaje de error.
  9. Se indica que la propiedad género puede tener un valor comprendido entre los que se encuentra en el enum. Además se indica que dicha propiedad debe ser un array, por lo que se puede indicar más de uno.
  10. Se valida el cuerpo de la petición
  11. Se comprueba si existe un error en el resultado de la validación, y en caso afirmativo se devuelve una respuesta con el mensaje de errorb