API con Node.js, Express y Prisma

Express

¿Qué es Express?

Express es una librería con un alto nivel de popularidad en la comunidad de Node.js e inclusive esta misma adoptó Express dentro de su ecosistema, pero esto no quiere decir que sea la única o la mejor, aunque existen muchas otras librerías y frameworks para crear REST API, Express si es sencilla y a su vez poderosa, según la definición en la página Web de Express JS: “Con miles de métodos de programa de utilidad HTTP y middleware a su disposición, la creación de una API sólida es rápida y sencilla.”

Configurar Express

Lo primero que debemos hacer es instalar el módulo de express como una dependencia de la aplicación:

npm install express

Esto añadirá una nueva entrada en el archivo package.json en la sección de dependencies indicando la versión instalada.

Seguido crearemos un nuevo archivo llamado index.js dentro del directorio app (que es realmente donde quedará nuestra aplicación):

touch app/index.js

De la misma manera que en la sección anterior comenzaremos tomando el ejemplo de Hello World que se encuentra en la documentación de Express y reemplazamos el contenido del archivo por el siguiente código:

// app/index.js

import express from 'express';

const port = 3000;

const app = express();

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}!`);
});

Esto es muy similar al servidor Web que se creó anteriormente en la raíz del proyecto, pero con unas sutiles diferencias:

  • Se utiliza la librería de express en vez del módulo http.
  • Se crea una aplicación de express (inclusive se pueden crear varias) y se almacena en la variable app.
  • Esta vez solo se aceptan peticiones en la raíz de la API (/) y solo con el verbo GET.

En la Terminal donde se está ejecutando el servidor Web anterior, se detiene con la combinación de teclas CTRL+C, y se prueba este nuevo servidor con el comando node app/index, luego accedemos a la siguiente dirección http://localhost:3000/ desde el navegador Web.

Aparentemente, vemos el mismo resultado, pero express ha brindado un poco más de simplicidad en el código y ha incorporado una gran cantidad de funcionalidades, las cuales veremos en las próximas secciones.

Más información:

Independizar la aplicación

Antes de continuar es importante organizar la aplicación, creando diferentes módulos cada uno con su propósito específico, ya que esto permite separar la responsabilidad de cada uno de ellos, organizarlos en directorios para agrupar los diferentes módulos con funcionalidad común, así el archivo index.js será más ligero, legible y estructurado.

D>Para este tipo de aplicaciones NO es una buena práctica colocar todo el código en un solo archivo que sería difícil de mantener, esto se le conoce como aplicación monolítica.

Modificamos la aplicación de Express de la siguiente manera:

// app/index.js

import express from 'express';

export const app = express();

app.get('/', (req, res) => {
  res.send('Hello World!');
});

Ahora el módulo tiene una sola responsabilidad, que es generar y exportar la aplicación, la cual será independiente a la librería que se utilice, pues cuando se importe solo será app no express.

De vuelta en el archivo principal, se reescribe quitando las múltiples responsabilidades que tenía y dejándolo solo con la responsabilidad de iniciar cada uno de los componentes de la aplicación, utilizando los módulos creados anteriormente:

// index.js

import http from 'http';

import { configuration } from './app/config.js';
import { app } from './app/index.js';

const { port } = configuration.server;

const server = http.createServer(app);

server.listen(port, () => {
  console.log(`Server running at port: ${port}`);
});

La función createServer de la librería http es compatible con la versión del servidor de la aplicación app, esto es muy conveniente, ya que si en el futuro se fuera a incorporar un servidor https, solamente es incluir la librería y duplicar la línea de creación del servidor, usando la misma aplicación (app).

Cada vez que existe un cambio en la configuración es necesario reiniciar el servidor, se presiona CTRL+C en la Terminal e iniciamos nuevamente en modo desarrollo con npm run dev.

Si se accede en el navegador a la dirección http://localhost:3000 todo debe estar funcionando sin ningún problema.

Express middleware

Las funciones middleware son la pieza más importante de la librería Express, esta es su definición:

“Las funciones middleware son funciones que tienen acceso al objeto de la solicitud (req), al objeto de respuesta (res) y a la función de que permite saltar al siguiente middleware (next).

(Tomado de Utilización de middleware)

Para entender esto mejor veamos el siguiente fragmento de código del archivo app/index.js:

app.get('/', (req, res, next) => {
  res.send('Hello World');
});

La función get es el verbo HTTP que se define para este endpoint y tiene dos parámetros:

  1. La ruta que se declara, en este caso la raíz de la API (/).
  2. La función callback que se llamará cuando al servidor llegue un request en el ejemplo anterior que coincida con la ruta raíz y el verbo GET.

Esta función callback es un middleware que tiene acceso al objeto req (request) que es la petición entrante y el objeto res (response) para darle respuesta al usuario. next es otra función que al invocarse permite continuar el flujo para que otras funciones middleware puedan continuar con el procesamiento de la petición, si es el caso.

En conclusión, cada middleware sé específica la petición que va a procesar según su declaración y parámetros.

¿Qué pasaría en este momento si el usuario hace una petición diferente a las declaradas en los endpoints? La librería de Express arrojaría un código 404, pero este permite ceder el control al programador al permitir agregar otro middleware al final de la declaración de todos los endpoints para capturar si la petición no fue procesada por ningún middleware definido anteriormente:

// app/index.js

import express from 'express';

export const app = express();

app.get('/', (req, res, next) => {
  res.send('Hello World!');
});

// No route found handler
app.use((req, res, next) => {
  res.status(404);
  res.json({
    message: 'Error. Route not found',
  });
});

Nótese varias cosas importantes en el fragmento de código anterior:

  1. A diferencia del endpoint anterior, este no tiene ruta, lo cual le indica a Express que si llega a esa instancia es porque no se dio respuesta al usuario en los middlewares declarados anteriormente.
  2. Se ha agregado la función next al middleware de la ruta raíz, pero realmente no se está utilizando dentro de la función, esto es con el fin de hacer concordancia con la firma que tienen las funciones middleware.
  3. Se indica a la aplicación (app) usar una función middleware, nuevamente se sabe que es una función middleware de Express por su firma, revisando los parámetros.
  4. Se establece el código HTTP de la respuesta con res.status(404) el cual es equivalente a una página no encontrada, en este caso una ruta o recurso.
  5. Cuando un servidor Web responde, debe indicar el tipo de contenido que está devolviendo al cliente para que esté a su vez pueda interpretarlo y mostrarlo al usuario, esto se indica en la respuesta mediante el encabezado Content-type, por defecto el valor es text/plain que se emplea para texto plano o text/html para código HTML, cada uno de estos valores se conoce como el MIME Type de los archivos. En este caso se responde explícitamente un objeto tipo JSON como se indica en la función res.json(), no hay necesidad de establecer el Content-Type, pues Express lo hace automáticamente y lo establece a application/json y adicionalmente convierte el objeto literal de JavaScript a un objeto JSON como tal.

Si guardamos el archivo se reiniciará el servidor Web, esto debido a la opción --watch y al acceder a una ruta que no existe como http://localhost:3000/unknown se obtiene el mensaje establecido en el middleware esta se convierte en la última función middleware de todos las declaradas anteriormente en esta aplicación de Express:

{
  message: 'Error. Route not found';
}

Para hacer una analogía a la definición de los middlewares es como una cola de funciones donde la petición se revisa comenzando desde la primera función, si el middleware coincide con la petición, procesa la petición y puede hacer dos cosas: dar respuesta a la petición y en este caso se detiene la revisión por completo o dejar pasar la petición al siguiente middleware esto se hace con la función next, o de lo contrario si ninguna coincide llegará al final que es el middleware genérico para capturar todas las peticiones que no se pudieron procesar antes.

Manejo de errores

Ahora surge la siguiente pregunta, ¿Cómo se puede controlar los errores o excepciones que sucedan dentro de los middlewares?, Express permite definir una función middleware con una firma de parámetros un poco diferente a las anteriores, la agregamos al final del archivo:

// app/index.js

import express from 'express';

export const app = express();

app.get('/', (req, res) => {
  res.send('Hello World!');
});

// No route found handler
app.use((req, res, next) => {
  res.status(404);
  res.json({
    message: 'Error. Route not found',
  });
});

// Error handler
app.use((err, req, res, next) => {
  const { status = 500, message } = err;

  res.status(status);
  res.json({
    error: {
      status,
      message,
    },
  });
});

Revisemos este nuevo middleware de Error:

  1. El middleware que captura errores en express comienza por el parámetro de error abreviado como err esto debido a la convención de los callbacks que tienen los callbacks en Node.js.
  2. Obtenemos el status del objeto err, si no trae ninguno, lo establecemos en 500, que por defecto significa Internal Server Error, así como el mensaje (message) que este trae.
  3. Finalmente, se establece el status en la respuesta (res) y se envía de vuelta un JSON con el mensaje del error.

Cada vez que se invoque un error dentro de cualquier middleware se tendrá uno que lo capture y procese, se aprovecha esto, por ejemplo, para guardar en los logs información relevante del error.

Ahora que se tiene una función de middleware para capturar la excepciones y errores, también se puede invocar desde otros middlewares, una de las funciones de la función next es esta misma: invocar el middleware de error:

// app/index.js

import express from 'express';

export const app = express();

app.get('/', (req, res) => {
  res.send('Hello World!');
});

// No route found handler
app.use((req, res, next) => {
  next({
    status: 404,
    message: 'Error. Route not found',
  });
});

...

I> Los tres puntos (…) es una convención para indicar el resto del contenido del archivo.

Ahora, si se invoca la función next con un argumento, este saltará directamente al siguiente middleware de error, además se centraliza en un solo middleware el manejo de errores y excepciones.

Para terminar se cambia nuevamente la definición de la ruta raíz para que todos los middlewares devuelvan formato JSON:

// app/index.js

import express from 'express';

export const app = express();

app.get('/', (req, res) => {
  res.json({
    message: 'Welcome to the API',
  });
});

...