En las próximas líneas vamos a describir los pasos para realizar la implementación de una API REST que realizará las funciones de la capa de backend y será responsable de la gestión de datos de un sistema de gestiṍn de turnos o tickets.

Si deseas profundizar en cuanto a como funciona una API REST, sus elementos y particularidades, puedes consultar este artículo de freecodecamp.org, que describe de forma detallada los conceptos anteriormente mencionados.

image-2

Cuando construimos una API REST, debemos identificar que datos para gestionar, en nuestro caso, vamos a tomar como ejemplo un sistema de gestión de tickets, que tendría un modelo de datos similar al que vemos en el siguiente diagrama:

image
Modelo entidad-relación. Sistema de turnos/tickets

Tomando en cuenta lo indicado, a partir del modelo entidad-relación anterior, es importante identificar los elementos que van a actuar como componentes de nuestra API REST.

Elementos componentes de una API REST

Vamos a identificar 3 componentes principales en la construcción de una API REST, estos son:

  • Modelos, que serán las representación de las entidades que iremos a gestionar en la API, es la abstracción de los datos del mundo real al mundo digital.
  • Controladores, responsables de implementar las operaciones a realizar sobre los modelos, a veces denominada capa de lógica de negocios.
  • Rutas, definiciones de como nuestra API puede interactuar con el mundo exterior, es decir a través de las rutas definimos la comunicación entre la implementación que vamos a realizar con los servicios o herramientas que irán a consumir lo que realicemos.

Para construir esta API vamos a usar NodeJS, que es el entorno de ejecución de javascript para backend.

Definiendo a NodeJS

Según la web oficial de nodejs.org, la definición de Node es:

Ideado como un entorno de ejecución de JavaScript orientado a eventos asíncronos, Node.js está diseñado para crear aplicaciones network escalables.

En el siguiente material tendrás algunas orientaciones básicas de como funciona y como realizar la instalación del mismo.

Qué es Fastify

Partiendo de los elementos indicados como componentes de una API, vamos a explicar que paquetes vamos a utilizar para la implementacion de cada uno de ellos. Comencemos por el responsable de las rutas o mejor en forma general, por la comunicación y transporte de datos.

Es a través de este proceso (comunicación y transporte) que nuestra API REST  va a recibir y enviar datos a quien esté consumiendo el servicio. Esto se hace a través de rutas.

Para esta tarea tenemos varias opciones, como usar las clases nativas de NodeJS, usar el framework expressjs o  en nuestro caso usar el framework fastify. Te preguntaras por qué Fastify?. Varias razones: simplicidad, rapidez y escabilidad del framework.

Primero vamos a crear nuestro proyecto node, con la siguiente instrucción:

npm init

Ahora avancemos a la instalación de fastify, esta muy simple, sólo ejecutanos el siguiente comando y con ello ya en nuestro proyecto podemos usar sus métodos y funcionalidades, pero comencemos por crear el proyecto:

npm install fastify

Ahora procedemos a construir el esqueleto de nuestra API.

Construyendo los elementos de nuestra API REST

Empecemos por el archivo principal, el cual será: index.js El contenido del archivo index.js será:

import Fastify from 'fastify';

const fastify = Fastify({
    logger: true
});

// Ruta inicial
fastify.get('/', async function handler (req, res) {
    return { hello: 'world' };
});
  
// Ejecución de la aplicación
try {
    await fastify.listen({ port: 3000 });
} catch (err) {
    fastify.log.error(err);
    process.exit(1);
}

Con lo anterior, ya tenemos en pocas líneas, una API funcional, que solo tiene una ruta, pero que en esencia está completa. Ahora pasemos a la extensión de sus rutas.

Definiendo las rutas básicas

Procedemos entonces a definir las rutas que nuestra API va a implementar, en este caso creamos un nuevo archivo llamado routes.js, en el cual definimos los CRUDs de cada uno de nuestros modelos, siendo que CRUD será (C-reate, R-ead, U-pdate, D-elete), en español serán las operaciones de Inserción, Lectura, Actualización y Borrado.

El archivo quedará de esta forma:

const rutas = [
    {
        method: "POST",
        url: "/usuarios",
        handler: async (req, res) => {
            res.status(200).send({status: 'OK - POST'});
        }
    },
    {
        method: "GET",
        url: "/usuarios",
        handler: async (req, res) => {
            res.status(200).send({status: 'OK - GET'});
        }
    },
    {
        method: "PUT",
        url: "/usuarios/:id",
        handler: async (req, res) => {
            res.status(200).send({status: 'OK - PUT'});
        }
    },
    {
        method: "DELETE",
        url: "/usuarios/:id",
        handler: async (req, res) => {
            res.status(200).send({status: 'OK - DELETE'});
        }
    }
]

export default rutas;

Observa que algunas rutas tienen :/id, esto lo explicaremos en detalle más adelante.

Con las rutas ya definidas, el siguiente paso será definir los modelos, notese que en el archivo anterior, dejamos la propiedad handler apuntando a una función básica que siempre retorna OK.

handler: (req,res) => {
	res.status(200).send('OK');
}

Esto significa que por ahora no tenemos ningún proceso asociado a las rutas, pero hemos dejado ya la estructura hecha. La implementación será realizada más adelante.

Incorporemos las rutas a nuestro archivo principal index.js, quedando este de la siguiente forma:

import Fastify from 'fastify';
import rutas from './rutas.js';
import cors from '@fastify/cors';

const fastify = Fastify({logger:true});
await fastify.register(cors, {

});

//POST - Inserciones                C-REATE
//GET - Consulta                    R-EAD
//PUT, PATCH - Actualizaciones      U-PDATE
//DELETE - Borrado                  D-ELETE

fastify.get("/",async function(req, res) {
    return { hello: 'world'};
});

//Incorporación de las rutas
rutas.forEach((ruta) => {
    fastify.route(ruta);
});

try {
    fastify.listen({port:3500});
} catch(erro) {
    console.log(erro);
}

En el siguiente material podrás visualizar el paso de la construcción que hemos hecho hasta ahora.

Modelos

Acá vamos a definir los datos que nuestra API procesará, tomemos la primera entidad "Usuarios" y definamos su modelo, para ello nos vamos a apoyar en sequelize, que nos permite hacer una definición que luego se reflejará en la base de datos, esto debido a que Sequelize actua como un ORM (Object Relation Mapping).

Pero qué es un ORM - Object Relation Mapping?

Tomando lo que https://en.wikipedia.com nos indica, podemos citar lo que es un ORM:

Un ORM (Object-Relational Mapping) es una técnica de programación que permite la conversión de datos entre una base de datos relacional y un lenguaje de programación orientado a objetos. En otras palabras, un ORM es una herramienta que ayuda a simplificar la traducción entre los dos paradigmas: objetos y tablas de bases de datos relacionales. Puede utilizar definiciones de clases (modelos) para crear, mantener y proporcionar acceso completo a los datos de los objetos y su persistencia en la base de datos.

Instalando Sequelizer

Para realizar la instalación de Sequelizer, necesitamos ejecutar sólo este comnpm install sequelizerando:

npm install sequelizer

Ejecutando lo anterior, tenemos ya disponible el paquete en nuestro proyecto, es momento de conectar nuestra base de datos, para ello creamos un nuevo archivo llamado db.js y colocamos los parámetros de conexión correspondientes:

/* eslint-disable require-jsdoc */
import Sequelize from 'sequelize';

/*
Clase de conexión a la base de datos
*/
class DBInstance {
	constructor() {
		const dbCfg = {
			user: '<usuario_base_datos>',
			host: '<ip_o_dns_servidor>',
			database: '<nombre_base_datos>',
			password: '<clave_usuario_base_datos>',
			port: 5432,
		};
		this.sequelize = new Sequelize(dbCfg.database, dbCfg.user, dbCfg.password, {
			host: dbCfg.host,
			dialect: 'postgres',
			logging: false,
		});
	}
}

export default new DBInstance().sequelize;

Con lo anterior, ya tenemos conexión a nuestra base de datos. Ajustamos nuestro archivo index.js para importar este proceso de conexión y dejarlo ejecutandose cuando sea iniciada la API. Quedando su código de esta forma:

import Fastify from 'fastify';
import db from './db.js';
import rutas from './rutas.js';
import cors from '@fastify/cors';

const fastify = Fastify({logger:true});
await fastify.register(cors, {

});

//POST - Inserciones                C-REATE
//GET - Consulta                    R-EAD
//PUT, PATCH - Actualizaciones      U-PDATE
//DELETE - Borrado                  D-ELETE

// eslint-disable-next-line no-unused-vars
fastify.get('/',async function(req, res) {
	return { hello: 'world'};
});

//Incorporación de las rutas
rutas.forEach((ruta) => {
	fastify.route(ruta);
});

async function database() {
	try {
		await db.sync();
		console.log('Conectado a la base de datos');
	} catch(e) {
		console.log(e);
	}
}

try {
	fastify.listen({port:3500});
	database();
} catch(erro) {
	console.log(erro);
}

Observa que hemos creado una función asincrona llamada database() y luego al iniciar la API, la invocamos para que se realice la conexión y sincronización del modelo.

Ahora procedemos a crear el modelo, recordando que un modelo es la representación de los datos que vamos a gestionar del mundo real en nuestra API.

Comencemos por el modelo de Usuarios:

import sequelize from "sequelizer";

const User = sequelize.define("User", {
    id : {
        type: sequelize.STRING,
        allowNull: false,
        primaryKey: true,
    },
    fullname: {
        type: sequelize.STRING,
        allowNull: false,
    },
    password: {
        type: sequelize.STRING,
        allowNull: false,
    },
});

export default User;

Observa que cada propiedad del objeto que define el modelo, corresponde con el nombre del campo que definimos en nuestro modelo de entidad-relación al inicio del documento, el nombre de cada propiedad representa cada campo, así que debemos tener cuidado con el nombre que le indiquemos, mi recomendación usar letras minusculas, no usar caracteres especiales.

En el caso de password, vamos usar un paquete adicional, para poder encriptar el contenido de la misma, recordando que por seguridad, los datos de claves no se deben guardar de forma plana o legible en la base de datos.

Para lograr que nuestro password quede guardado de forma segura, hacemos uso del paquete bcrypt, el cual nos permite codificar el valor enviado por el usuario y luego con un método propio del paquete podemos validar si clave es la guardada, de esta forma el modelo de Usuarios queda implementado con el siguiente código:

import sequelize from 'sequelize';
import bcrypt from 'bcrypt';
import db from '../db.js';


const UserModel = db.define('users', {
  id: {
    type: sequelize.STRING,
    allowNull: false,
    primaryKey: true,
  },
  fullname: {
    type: sequelize.STRING,
    allowNull: false,
  },
  password: {
    type: sequelize.STRING,
    allowNull: false,
  },
}, {
  hooks: {
    beforeCreate: (user) => {
      const salt = bcrypt.genSaltSync();
      user.password = bcrypt.hashSync(user.password, salt);
    },
  },
});

UserModel.prototype.validPassword = function(password) {
  return bcrypt.compareSync(password, this.password);
};

export default UserModel;
Modelo de Usuarios

Para el caso del modelo de tiendas o comercios, siguiendo lo definido anteriormente, el modelo queda representado con el siguiente código:

import sequelize from 'sequelize';
import db from '../db.js';

const StoreModel = db.define('stores', {
  id: {
    type: sequelize.INTEGER,
    autoIncrement: true,
    allowNull: false,
    primaryKey: true,
  },
  store_name: {
    type: sequelize.STRING,
    allowNull: false,
  },
  category_store: {
    type: sequelize.STRING,
    allowNull: false,
  },
});


export default StoreModel;
Modelo de Tiendas/Comercios

Algunas diferencias notorias, en el caso de id, usamos una propiedad dentro del objeto id que es autoIncrement, la cual define que el campo en la base de datos será del valor automático e incremental, de esta forma la base de datos asignará el id correspondiente. Otro punto interesante es el tipo de datos, en el caso de id usamos el tipo INTEGER, para definir que es un número entero.

Pasemos ahora al modelo de Tickets o Turnos, hacemos la implementación, recordando que este modelo se relaciona con Usuarios y Tiendas, de forma que un Usuario tiene N Tickets y una Tienda también tiene N Tickets, de esta forma el modelo implementado será:

import sequelize from 'sequelize';
import db from '../db.js';
import UserModel from './Users.js';
import StoreModel from './Stores.js';

const TicketModel = db.define('tickets', {
  id: {
    type: sequelize.STRING,
    allowNull: false,
    primaryKey: true,
  },
  date_time: {
    type: 'TIMESTAMP',
    allowNull: false,
  },
  status: {
    // eslint-disable-next-line new-cap
    type: sequelize.ENUM(['Creado', 'Confirmado', 'Atendido', 'Cancelado',
      'En progreso']),
    allowNull: false,
  },
  observation: {
    type: sequelize.TEXT,
    allowNull: false,
  },
  end_date_time: {
    type: 'TIMESTAMP',
    allowNull: false,
  },
});

UserModel.hasMany(TicketModel, {
  foreignKey: {
    name: 'user_id',
    type: sequelize.STRING,
    allowNull: false,
  },
});

StoreModel.hasMany(TicketModel, {
  foreignKey: {
    name: 'store_id',
    type: sequelize.INTEGER,
    allowNull: false,
  },
});


export default TicketModel;
Modelo de Turnos o Tickets

En este modelo tenemos algunas particularidades que valen la pena analizar:

  • Tenemos nuevos tipos de datos: TEXTpara almacenar grandes cantidad de datos (mayores a 255 caracteres), ENUM para definir un conjunto de valores cerrados para un campo en particular. TIMESTAMP para definir campos de tipo fecha/hora.
  • Relaciones, en este modelo le decimos a sequelize que queremos relacionar los modelos en este segmento de código:
UserModel.hasMany(TicketModel, {
  foreignKey: {
    name: 'user_id',
    type: sequelize.STRING,
    allowNull: false,
  },
});

StoreModel.hasMany(TicketModel, {
  foreignKey: {
    name: 'store_id',
    type: sequelize.INTEGER,
    allowNull: false,
  },
});

Observa que se indica que el modelo Usuarios (UserModel) tiene muchos Tickets con la instrucción: hasMany, lo mismo sucede en el caso de Tiendas (StoreModel), le indicamos que va a tener múlples Tickets.

Con ellos hemos finalizado los modelos a implementar, ahora pasemos a la lógica de negocios.

Controladores

Estos elementos tan importantes a la hora de construir nuestra API REST, son los responsables de implementar lo que denominamos la lógica de negocios, es decir ellos reciben los datos enviados por quienes consumen el servicio o la API, los procesan, envian a la capa de datos cuando corresponde y retornan una respuesta.

De acuerdo con lo anterior, entonces en los controladores donde vamos a implementar el famoso CRUD (Create, Read, Update and Delete), el cual ya citamos anteriormente. Ahora bien, definamos la anatomía de un controlador:

import Model from "../models/Model.js";

class ModeloController {
    constructor() {

    }
    
    //Creación de registros
    async create (req, res)  {
    }
    
    //Consulta de todos los registros del modelo
    async getAll(req, res) {
    }

	//Consulta de un registro en particular
    async getOne(req, res) {
    }
    
    //Actualización de un registro
    async update(req, res) {
    }
    
    
    //Borrado de un registro
    async delete(req, res) {
    }
    
 }
    

Notemos que el controlador implementar como comentarmos anteriormente cada una de las operaciones del CRUD, que la API va a realizar para cada uno de los modelos construidos, de esta forma, vamos a tener que crear un controlador para usuarios, otro para tiendas y uno más para los tickets o turnos.

En este material realizamos la implementación de los modelos y parte de los controladores:

Ahora para cada modelo que hemos definido en nuestra API, construimos sus controladores, quedando su código de esta forma:

  • UsersController
/* eslint-disable require-jsdoc */
import UserModel from '../models/Users.js';

class UsersController {
  constructor() {

  }

  async create(req, res) {
    try {
      const userModel = await UserModel.create(req.body);

      if (userModel) {
        res.status(200).send({status: true, id: userModel.id});
      }
    } catch (e) {
      res.status(500).send({error: e});
    }
  }

  async getAll(req, res) {
    try {
      const where = {...req.query};
      const usuarios = await UserModel.findAll({where});
      res.status(201).send(usuarios);
    } catch (err) {
      res.status(500).send(
          {message: err.message || 'Error al consultar usuarios'},
      );
    }
  }

  async getOne(req, res) {
    try {
      const {id} = req.params;

      const userModel = await UserModel.findByPk(id);

      if (userModel) {
        res.status(201).send(userModel);
      } else {
        res.status(404).send(
            {message: 'Registro no encontrado'},
        );
      }
    } catch (err) {
      res.status(500).send(
          {message: err.message || 'Error al consultar el usuario'},
      );
    }
  }

  async update(req, res) {
    try {
      const {id} = req.params;

      const userModel = await UserModel.update(req.body,
          {where: {id}});

      if (typeof (userModel[0]) !== 'undefined' && userModel[0] === 1) {
        res.status(201).send({
          status: true,
        });
      } else {
        res.status(404).send(
            {message: 'Registro no encontrado'},
        );
      }
    } catch (err) {
      res.status(500).send(
          {message: err.message || 'Error al actualizar el usuario'},
      );
    }
  }

  async delete(req, res) {
    try {
      const {id} = req.params;

      const userModel = await UserModel.destroy({where: {id}});

      if (userModel) {
        res.status(201).send({
          status: true,
        });
      } else {
        res.status(404).send(
            {message: 'Registro no encontrado'},
        );
      }
    } catch (err) {
      res.status(500).send(
          {message: err.message || 'Error al intentar borrar un usuario'},
      );
    }
  }
}

export default new UsersController();

Del código anterior podemos resaltar algunas caracteristicas:

  • Todos los métodos son asincronos, es decir por ser operaciones de base de datos, estos procesos deben esperar resultados y recordamos que Javascript no espera a nadie por lo tanto usamos las palabras reservadas async e await para lograr ese objetivo.
  • Encerramos el procesamiento o la lógica de cada operación entre las palabras try y catch de forma tal que cualquier error, lo podamos capturar y retornar a quien nos consume los servicios.
  • En cada uno de los métodos de nuestros controladores, nos vamos a apoyar en métodos de Sequelize, que nos van a facilitar el acceso y la gestión de los datos a nivel de base de datos. En la creación usamos create, en la búsqueda usamos findAll, para conseguir un registro en particular usamos findByPk, para actualizar usamos update y para borrar usamos destroy.
  • En las operaciones que envuelven acceso a un registro en particular usamos del objeto req la propiedad params, es decir usamos req.params para obtener el id del registro. Este parámetro lo definimos en la ruta.
  • Cuando hacemos operaciones de envio de datos, sea en la creación o en la actualización, el cuerpo o data la recibimos en lo que se denomina el body de la requisición, esto se obtiene con req.body.

Veamos como ha quedado el controlador de tiendas:

import StoreModel from '../models/Stores.js';


/* eslint-disable require-jsdoc */
class StoreController {
  constructor() {

  }

  async create(req, res) {
    try {
      const storeModel = await StoreModel.create(req.body);

      if (storeModel) {
        res.status(201).send({status: true, id: storeModel.id});
      }
    } catch (e) {
      res.status(500).send(e);
    }
  }

  async getAll(req, res) {
    try {
      const where = req.query;

      const tiendas = await StoreModel.findAll({where});
      res.status(201).send(tiendas);
    } catch (err) {
      res.status(500).send(
          {message: err.message || 'Error al consultar tiendas'},
      );
    }
  }

  async getOne(req, res) {
    try {
      const {id} = req.params;

      const storeModel = await StoreModel.findByPk(id);
      if (storeModel) {
        res.status(201).send(storeModel);
      } else {
        res.status(404).send(
            {message: 'Registro no encontrado'},
        );
      }
    } catch (err) {
      res.status(500).send(
          {message: err.message || 'Error al consultar la tienda'},
      );
    }
  }

  async update(req, res) {
    try {
      const {id} = req.params;
      const data = {...req.body};

      delete data.id;

      const storeModel = await StoreModel.update(data,
          {where: {id}});

      if (typeof (storeModel[0]) !== 'undefined' && storeModel[0] === 1) {
        res.status(201).send({
          status: true,
        });
      } else {
        res.status(404).send(
            {message: 'Registro no encontrado'},
        );
      }
    } catch (err) {
      res.status(500).send(
          {message: err.message || 'Error al actualizar la tienda'},
      );
    }
  }

  async delete(req, res) {
    try {
      const {id} = req.params;

      const storeModel = await StoreModel.destroy({where: {id}});

      if (storeModel) {
        res.status(201).send({
          status: true,
        });
      } else {
        res.status(404).send(
            {message: 'Registro no encontrado'},
        );
      }
    } catch (err) {
      res.status(500).send(
          {message: err.message || 'Error al intentar borrar una tienda'},
      );
    }
  }
}

export default new StoreController();

El controlador es bien similar al de usuarios, implementado en la clase UserController, solo en el método de actualización (update), agregamos está linea: delete data.id; para evitar que sea modificar el id interno del registro.

El controlador de Turnos, sigue a continuación, este controlador también tiene la protección para evitar que el campo autonumérico sea modificado en el método de actualización (update).

import TicketModel from '../models/Tickets.js';

/* eslint-disable require-jsdoc */
class TicketController {
	constructor() {

	}

	async create(req, res) {
		try {
			const ticketModel = await TicketModel.create(req.body);

			if (ticketModel) {
				res.status(201).send({status: true, id: ticketModel.id});
			}
		} catch (e) {
			res.status(500).send(e);
		}
	}

	async getAll(req, res) {
		try {
			const where = req.query;

			const tickets = await TicketModel.findAll({where});
			res.status(201).send(tickets);
		} catch (err) {
			res.status(500).send(
				{message: err.message || 'Error al consultar tickets'},
			);
		}
	}

	async getOne(req, res) {
		try {
			const {id} = req.params;

			const ticketModel = await TicketModel.findByPk(id);
			if (ticketModel) {
				res.status(201).send(ticketModel);
			} else {
				res.status(404).send(
					{message: 'Registro no encontrado'},
				);
			}
		} catch (err) {
			res.status(500).send(
				{message: err.message || 'Error al consultar el ticket'},
			);
		}
	}

	async update(req, res) {
		try {
			const {id} = req.params;
			const data = {...req.body};
			delete data.id;

			const ticketModel = await TicketModel.update(data,
				{where: {id}});

			if (typeof (ticketModel[0]) !== 'undefined' && ticketModel[0] === 1) {
				res.status(201).send({
					status: true,
				});
			} else {
				res.status(404).send(
					{message: 'Registro no encontrado'},
				);
			}
		} catch (err) {
			res.status(500).send(
				{message: err.message || 'Error al actualizar el ticket'},
			);
		}
	}

	async delete(req, res) {
		try {
			const {id} = req.params;

			const ticketModel = await TicketModel.destroy({where: {id}});

			if (ticketModel) {
				res.status(201).send({
					status: true,
				});
			} else {
				res.status(404).send(
					{message: 'Registro no encontrado'},
				);
			}
		} catch (err) {
			res.status(500).send(
				{message: err.message || 'Error al intentar borrar un ticket'},
			);
		}
	}
}

export default new TicketController();

Con esto hemos finalizado los controladores.

Rutas actualizadas

Ahora es momento de revisar como llamamos todo lo implementado en nuestras rutas, recordando que al inicio generamos las rutas para el módelo de usuarios, pero llamando a métodos genéricos en lugar de nuestros controladores, con ellos implementados, podemos ajustar el código, el cual queda de la siguiente forma:

import UsersController from './controllers/Users.js';
import StoreController from './controllers/Stores.js';
import TicketController from './controllers/Tickets.js';

const rutas = [
	{
		method: 'POST',
		url: '/usuarios',
		handler: UsersController.create,
	},
	{
		method: 'GET',
		url: '/usuarios',
		handler: UsersController.getAll,
	},
	{
		method: 'GET',
		url: '/usuarios/:id',
		handler: UsersController.getOne,
	},
	{
		method: 'PUT',
		url: '/usuarios/:id',
		handler: UsersController.update,
	},
	{
		method: 'DELETE',
		url: '/usuarios/:id',
		handler: UsersController.delete,
	},

	/* Rutas de tiendas */
	{
		method: 'POST',
		url: '/tiendas',
		handler: StoreController.create,
	},
	{
		method: 'GET',
		url: '/tiendas',
		handler: StoreController.getAll,
	},
	{
		method: 'GET',
		url: '/tiendas/:id',
		handler: StoreController.getOne,
	},
	{
		method: 'PUT',
		url: '/tiendas/:id',
		handler: StoreController.update,
	},
	{
		method: 'DELETE',
		url: '/tiendas/:id',
		handler: StoreController.delete,
	},

	/* Rutas de turnos */
	{
		method: 'POST',
		url: '/turnos',
		handler: TicketController.create,
	},
	{
		method: 'GET',
		url: '/turnos',
		handler: TicketController.getAll,
	},
	{
		method: 'GET',
		url: '/turnos/:id',
		handler: TicketController.getOne,
	},
	{
		method: 'PUT',
		url: '/turnos/:id',
		handler: TicketController.update,
	},
	{
		method: 'DELETE',
		url: '/turnos/:id',
		handler: TicketController.delete,
	},
];

export default rutas;

Interesante que nuestro archivo raiz index.js no sufre ninguna alteración, esto debido al grado de encapsulamiento y desacoplamiento que hemos implementado.

Con esto ya tenemos una API Rest funcional, implementada con NodeJS, Fastify, Sequelize y PostgreSQL.

Puedes verificar la implementación de los controladores y de todos sus métodos en este material: