Backend For Frontend Pattern

Bienvenidos de nuevo!! He estado últimamente muy ocupado (vacaciones…) y es por ésto por lo que este siguiente artículo viene con algo de retraso respecto a los anteriores. Aún así vengo con pilas renovadas y dispuesto a enseñaros otra nueva forma o patrón software con el que podremos comunicar distintos clientes de nuestras aplicaciones utilizando un único servicio como fuente de la verdad y otros servicios como intercontectores entre éste y las aplicaciones cliente, así conseguiremos evitar que en nuestro servicio tengamos que crear todos los Enpoints necesarios para que ambos clientes funcionen con normalidad.

Para resolver el problema anteriormente expuesto existen varias formas y en esta ocasión la que vamos a tratar es el patrón Backend For Frontend (BFF). Este patrón surgen con el enfoque de una arquitectura de microservicios, en donde disponemos de distintos servicios cada uno gestionando una parte muy concreta de nuestro negocio, y como antes detallaba el problema surge cuando en ésta ecuación añadimos las aplicaciones cliente, ya sean Web, Móvil, Escritorio…etc.

Os recuerdo que el código mostrado en el artículo se encuentra en el siguiente repositorio de código: https://github.com/xXjoakinXx/MindBodyAndCode2020.git .Y dicho todo este preludio nos ponemos manos a la obra!

Caso de ejemplo

Este patrón básicamente se centra en la adición de un tercer servicio entre el servicio que contiene el acceso a los datos (BD) y la aplicación cliente que se va a nutrir de dichos datos. Lo que realmente realiza este tercer servicio no es otra acción nada más que de intermediario entre la App cliente y el susodicho servicio (o varios servicios…), haciendo de adaptador entre la forma de acceder o pedir los datos del cliente en el ecosistema de Backend que tengamos desarrollado. Pero no solo eso, si no que además también nos sirve para agregar o agrupar la información que tengamos disponible entre los distintos servicios, para ilustrarlo mejor imaginemos el siguiente ejemplo.

Imaginemos el caso ilustrado en el que disponemos de una aplicación REST que gestiona Customers, y por otro lado, disponemos también de dos aplicaciones cliente distintas, una versión en una App para el móvil y otra versión Web. Ambas aplicaciones realizan peticiones hacia el mismo servicio, ya que se nutren de la misma información pero con una particularidad y es que la App lo único que necesita es un objeto (Json,por ejemplo) que contenga únicamente la información del nombre del usuario. Así que nos disponemos a crear el Endpoint GET conveniente, al cual realizar una consulta y nos recupera esa información ni más ni menos. Hasta ahora todo perfecto, pero entonces pasamos a elaborar la aplicación cliente Web, desde donde por motivos ‘X’ únicamente necesitamos como información del Customer su DNI… bueno no pasa nada creamos en el servicio Customer REST API otro Endpoint GET que exponga la información de ésta otra forma y listo.

Pasa el tiempo y para la App ahora el cliente nos exige que también necesitamos el domicilio del Customer, y al mismo tiempo desde la aplicación Web nos solicitan las ubicaciones de empadronamiento, bueno vamos a crear los Endpoint relacionados y … ¿Ves ya el problema? Conforme las necesidades de cada aplicación cliente van cambiando y evolucionando con el tiempo también lo tiene que hacer nuestro servicio de Backend (Obviamente siempre tiene que hacerlo…), pero el principal problema es que estaremos acoplando indistintamente el servicio Customer REST API a ambas aplicaciones con cada Endpoint que vamos creando ya que la información la construimos de dos formas diferentes según nos lo van pidiendo. Y no solo eso, imaginemos que ahora viene una aplicación de escritorio que también tiene que recibir información de nuestro servicio … sin darnos cuenta estamos creando una API REST sobredimensionada y con una cantidad relevante de Endpoint duplicados.

Primera aproximación: Filtrado por cliente

También podrías pensar que no pasa nada, haces los mismos Endpoint que sirvan para las tres aplicaciones cliente y ya esta, pero entonces tendrás que lidiar con el problema de que cualquier cambio o extensión en el servicio REST estaría afectando al mismo tiempo a todas las aplicaciones clientes, podría ponerse bastante complicado con el tiempo. Veamos a continuación como el patrón BFF nos soluciona dicho problema.

Con la simple inclusión de un tercer elemento, dos en éste caso uno para la App y otro para la versión Web, habilitamos la posibilidad de que ambas partes Frontend y Backend puedan evolucionar independientemente con la tranquilidad de que no estaremos entorpeciendo o acoplándonos entre los dos ámbitos. Estos dos nuevos servicios BFF Mobile App y BFF Web, serían los encargados de adaptar tanto las peticiones al servicio subyacente como las correspondientes respuestas de este, moldeando el resultado a cómo la aplicación cliente los necesita. Veamos un ejemplo en código muy sencillo .

CustomerAPI

    [ApiController]
    [Route("[controller]")]
    public class CustomerController : ControllerBase
    {
        private static readonly Customer[] _customers = new Customer[]{ 
            new Customer() { CustomerId = Guid.NewGuid(), 
                             Dni = "77846465R", 
                             Name = "Antonio"},
            new Customer() { CustomerId = Guid.NewGuid(), 
                             Dni = "33472726T", 
                             Name = "Francisco"}
            };

        private readonly ILogger<CustomerController> _logger;

        public CustomerController(ILogger<CustomerController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public ActionResult<IEnumerable<Customer>> Get()
        {
            return Ok(_customers);
        }
   }
    [Serializable]
    public class Customer
    {
        public Customer()
        {

        }

        public Guid CustomerId { get; set; }
        public string Dni { get; set; }
        public string Name { get; set; }
    }

Este sería el código de ejemplo para la aplicación CustomerAPI, no es nada más que una aplicación web Net Core 3.1, la cual consta de un controlador CustomerController donde exponemos un Endpoint simulando el acceso a una BD y retornando una serie de recursos relacionados con la entidad Customer que constan de datos de un cliente como son su ID, DNI y por último su nombre. Si realizamos una llamada al servicio obtendríamos un resultado como el siguiente:

Como vemos se expone el modelo completo de datos en formato Json y consta de dos elementos Customer. Pues bien ahora implementamos el siguiente servicio para la App móvil usando el patrón BFF.

BFFMobileAPP

    [ApiController]
    [Route("[controller]")]
    public class CustomerController : ControllerBase
    {
        private string _customerAPIUrl = "https://localhost:44398/Customer";

        private readonly ILogger<CustomerController> _logger;
        private readonly IHttpClientFactory _httpClientFactory;

        public CustomerController(ILogger<CustomerController> logger, IHttpClientFactory httpClientFactory)
        {
            _logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
            _httpClientFactory = httpClientFactory ?? 
                 throw new System.ArgumentNullException(nameof(httpClientFactory));
        }

        [HttpGet]
        public async Task<ActionResult<IEnumerable<CustomerDto>>> Get()
        {
            var request = new HttpRequestMessage(HttpMethod.Get, _customerAPIUrl);

            var client = _httpClientFactory.CreateClient();

            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                var responseString = await response.Content.ReadAsStringAsync();
                var customerDto = JsonConvert.DeserializeObject<IEnumerable<CustomerDto>>(responseString);
                return Ok(customerDto);
            }
            else
                return NoContent();
        }
    }
    [Serializable]
    public class CustomerDto
    {
        public CustomerDto()
        {
                
        }

        public Guid CustomerId { get; set; }

        public string Name { get; set; }
    }

Un Ejemplo del resultado de una llamada desde la aplicación móvil sería como el siguiente:

En éste caso el servicio que aplica el patrón BFF para la aplicación cliente del móvil, consta nada menos que de un controlador denominado exactamente igual que el que disponíamos en CustomerAPI (Estamos trabajando sobre el mismo dominio), donde lo único que estamos haciendo es definir un DTO (Por si alguien no se acuerda de lo que es) donde restringimos el dominio que la App necesita, y ésta solo requerirá del campo Name del cliente, con lo que creamos el objeto a transmitir acorde a los requisitos. Y además incluimos un Endpoint GET desde donde realizaremos una llamada al servicio de Backend CusomerAPI obteniendo toda la información de los clientes, convirtiendo el DTO de respuesta en uno que cliente espera recibir.

El mismo ejemplo de patrón BFF para la aplicación Web sería:

BFFWeb

Omito el controlador CustomerController porque es idéntico que el de la aplicación anterior BFFMobileApp, para este ejemplo”

    [Serializable]
    public class CustomerDto
    {

        public CustomerDto()
        {

        }
        
        public Guid CustomerId { get; set; }

        public string Dni { get; set; }
    }

Y el resultado de la llamada en éste servicio para la aplicación Web sería como sigue:

Como vemos es el mismo resultado que la llamada anterior pero en éste caso obtenemos de los clientes su DNI en lugar del nombre, puesto que la aplicación Web así lo requiere.

El ejemplo de código actual es muy muy sencillito como habréis podido apreciar, y seguro que muchos no estaréis viendo realmente el potencial que éste patrón nos aporta visto en una escala tan diminuta. El objetivo es que se vea su utilidad partiendo desde lo más simple. Con esto las aplicaciones que hemos creado hacen de intermediarios entre las peticiones del servicio de Backend y los clientes, obteniendo así un punto central de adaptación, el cual nos sirve para perder el miedo a modificar nuestro servicio de Backend sin destrozar (Al menos de primera mano…) a los clientes conectados.

Segunda aproximación: Agregado de llamadas HTTP

Añadamos ahora otro pasito más al ejemplo anterior en donde vislumbraremos algo más de luz en el uso del patrón BFF, y es que, como antes mencionaba en una arquitectura de microservicios no disponemos de tan solo una aplicación de Backend, si no que constataremos un ecosistema de servicios que colaborarán entre sí y que servirán los datos de forma dispersa a los distintos clientes. Un posible ejemplo sería el incluir el servicio VehicleAPI, encargado de gestionar los vehículos que tienen en propiedad nuestros clientes:

Al ir generando más servicios en nuestro sistema la complejidad puede ir creciendo considerablemente, e imaginaros el caso de un cliente externo que quiera conectarse a nuestro sistema y nos pregunte a que URL debería de apuntar para poder nutrirse de nuestros servicios… ¿Cuantos EndPoints diferentes deberíais de indicarle, uno por servicio? Con el patrón BFF la situación se simplifica bastante. Y no solo eso, imaginemos ahora el caso en el que necesito información de ambos servicios y unificarla en la parte FrontEnd, el cliente no tiene porqué tener que llamar a varios servicios directamente, es por esto que crear un intermediario encargado de procesar la información y agruparla de forma acorde a como éste espera es seguramente una de las opciones idóneas para el caso en cuestión. O por el lado contrario contemos con que el cliente a veces deberá de consumir información de CustomerAPI y otras veces de VechileAPI, tampoco sería muy conveniente cuando el número de nuestros servicios sea considerable.

Veamos ahora ambos casos a nivel de código, veremos un ejemplo creando VehicleAPI y mostrando como desde BFF Mobile consumimos desde ambos servicios al mismo tiempo.

CusomerAPI

    [ApiController]
    [Route("[controller]")]
    public class CustomerController : ControllerBase
    {
        private static readonly Customer[] _customers = new Customer[]{ 
            new Customer() { CustomerId = Guid.Parse("0f8fad5b-d9cb-469f-a165-70867728950e"), Dni = "77846465R", Name = "Antonio"},
            new Customer() { CustomerId = Guid.Parse("7c9e6679-7425-40de-944b-e07fc1f90ae7"), Dni = "33472726T", Name = "Francisco"}
            };

        private readonly ILogger<CustomerController> _logger;

        public CustomerController(ILogger<CustomerController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public ActionResult<IEnumerable<Customer>> Get()
        {
            return Ok(_customers);
        }
    }

Se modifica el mock de los datos de Customer para que su Guid sea único.

VechicleAPI

    [ApiController]
    [Route("[controller]")]
    public class VehicleController : ControllerBase
    {
        private static readonly Vehicle[] _vehicles = new Vehicle[]{
            new Vehicle() { VechicleId = Guid.NewGuid(), CustomerId = Guid.Parse("0f8fad5b-d9cb-469f-a165-70867728950e"), Brand = "Renault", Enrollment = "9724HFH" },
            new Vehicle() { VechicleId = Guid.NewGuid(), CustomerId = Guid.Parse("7c9e6679-7425-40de-944b-e07fc1f90ae7"), Brand = "Audi", Enrollment = "3425KJD" }
            };


        private readonly ILogger<VehicleController> _logger;

        public VehicleController(ILogger<VehicleController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public ActionResult<IEnumerable<Vehicle>> Get()
        {
            return Ok(_vehicles);
        }
    }
    [Serializable]
    public class Vehicle
    {
        public Vehicle()
        {

        }

        public Guid VechicleId { get; set; }

        public Guid CustomerId { get; set; }

        public string Enrollment { get; set; }

        public string Brand { get; set; }
    }

Creamos la API encargada de administrar los vehículos de los clientes, mockeando de forma similar un conjunto de datos de vehículos asociándolos con sus correspondientes propietarios (Customer) mediante el Guid conveniente. Exponiendo además de forma similar el Endpoint que nos ofrecerá los datos.

BFFMobileApp

    [Serializable]
    public class CustomerDto
    {
        public CustomerDto()
        {
                
        }

        public Guid CustomerId { get; set; }

        public string Name { get; set; }

        public VehicleDto Vehicle { get; set; }
    }
    [Serializable]
    public class VehicleDto
    {
        public VehicleDto()
        {

        }

        public Guid CustomerId { get; set; }

        public Guid VechicleId { get; set; }

        public string Enrollment { get; set; }

        public string Brand { get; set; }
    }

Para el servicio que aplica el patrón BFF para la App mobile, creamos el conjunto de DTOs que será el transmitido a los clientes de dicha forma. Como podemos apreciar el DTO relativo a entidades Customer contiene una referencia al VechileDTO, donde posteriormente construiremos con la información de ambos servicios como veremos a continuación.

    [ApiController]
    [Route("[controller]")]
    public class CustomerController : ControllerBase
    {
        // Urls de ambos servicios de Backend
        private string _customerAPIUrl = "https://localhost:44398/Customer";
        private string _vehicleAPIUrl = "https://localhost:44384/vehicle";

        private readonly ILogger<CustomerController> _logger;
        private readonly IHttpClientFactory _httpClientFactory;

        public CustomerController(ILogger<CustomerController> logger, IHttpClientFactory httpClientFactory)
        {
            _logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
            _httpClientFactory = httpClientFactory ?? throw new System.ArgumentNullException(nameof(httpClientFactory));
        }

        [HttpGet]
        public async Task<ActionResult<IEnumerable<CustomerDto>>> Get()
        {
            //Realizamos ambas peticiones para obtener los datos en paralelo
            var requestToCustomerApi = new HttpRequestMessage(HttpMethod.Get, _customerAPIUrl);
            var requestToVechicleApi = new HttpRequestMessage(HttpMethod.Get, _vehicleAPIUrl);

            var client = _httpClientFactory.CreateClient();

            var customerResponseTask = client.SendAsync(requestToCustomerApi);
            var vehicleResponseTask = client.SendAsync(requestToVechicleApi);

            await Task.WhenAll(customerResponseTask, vehicleResponseTask);

            var customerResponse = customerResponseTask.Result;
            var vehicleResponse = vehicleResponseTask.Result;

            // Una vez obtenida ambas respuestas si todo ha ido correctamente...
            if (customerResponse.IsSuccessStatusCode && vehicleResponse.IsSuccessStatusCode)
            {
                // Se deserializan los DTOs resultado en sus correspondientes clases
                IEnumerable<CustomerDto> customerDtos = await DeseralizeResponse<CustomerDto>(customerResponse);
                IEnumerable<VehicleDto> vehicleDtos = await DeseralizeResponse<VehicleDto>(vehicleResponse);

                // En este punto mezclamos la información juntando cada Customer con su Vehicle
                var customerWithVehicleDtos = BuildResultDto(customerDtos, vehicleDtos);

                // Devolvemos al cliente el DTO compuesto :)
                return Ok(customerWithVehicleDtos);
            }
            else
                return NoContent();
        }

        // Construcción del DTO compuesto
        private IEnumerable<CustomerDto> BuildResultDto(IEnumerable<CustomerDto> customerDtos, IEnumerable<VehicleDto> vehicleDtos)
        {

            foreach(var customerDto in customerDtos)
            {
                customerDto.Vehicle = vehicleDtos.FirstOrDefault(v => v.CustomerId == customerDto.CustomerId);
            }

            return customerDtos;
        }

        private async Task<IEnumerable<T>> DeseralizeResponse<T>(HttpResponseMessage customerResponse)
        {
            var responseString = await customerResponse.Content.ReadAsStringAsync();
            var resultDto = JsonConvert.DeserializeObject<IEnumerable<T>>(responseString);
            return resultDto;
        }


    }

Por último en la llamada al Controller debemos de realizar los siguientes pasos:

  1. Realizar la llamada a los distintos servicios mediante un cliente REST en nuestro caso.
  2. Deserializamos los objetos Json de respuesta en una clase acorde.
  3. Juntamos la información obtenida por ambos servicios relacionándolos por sus identificadores en un DTO común.
  4. Devolvemos en la llamada HTTP hacia el cliente el resultado compuesto.

Con estos sencillos pasos estaríamos aplicando ya el patrón BFF, donde abstraemos a las aplicaciones cliente de la complejidad interna de nuestro sistema. Si ejecutamos todos los servicios y desde la aplicación BFFMobileApp realizamos la llamada HTTP obtendríamos el siguiente resultado:

Nota: He utilizado Swagger para poder tener una visualización más elegante del objeto Json resultado.

Se ve claramente el objeto resultante de la llamada y así comprobamos como el cliente es completamente agnóstico a los servicios que internamente entran en juego para que su petición tenga efecto. Así como hemos realizado dos llamadas simultáneas en ambos servicios podríamos hacer llamadas indistintamente a los servicios internos de nuestro sistema, y aun así para el cliente tan solo existe un punto de entrada, este otro tipo de acción también tiene su aplicación con el Patrón API Gateway el cual es ligeramente diferente a éste patrón, y además existe una tercera aproximación, la conjunción de ambos patrones en el mismo servicio BFF. Y no solo eso, también nos podemos encontrar con la problemática de tener que realizar peticiones entre los distintos servicio de forma transaccional, para éste tipo de situaciones existe el Patrón Saga. Pero esto son cosas que ampliaremos en siguientes publicaciones.

Realmente es posible establecer distintos tipos de arquitecturas usando el patrón BFF, una aproximación sería como nosotros acabamos de realizar, donde disponemos un servicio de BFF por cada cliente en nuestro sistema. Pero existen otros enfoques en el que se puede establecer un BFF por tipo de aplicación cliente, es decir, un BFF para aplicaciones móviles, otro para Webs, otro para aplicaciones de escritorio… etc.

Conclusión

El patrón Backend For Frontend nace de la necesidad de la diversidad de clientes sobre un mismo sistema de Backend, donde dichos clientes además requieren distintas especificaciones a la hora de consumir nuestros servicios. Claramente esta aproximación está muy arraigada con el uso de una arquitectura de microservicios, ya que, en éste tipo de infraestructuras la adición de nuevos servicios no debería de suponer un problema mayor ni mucho menos, si no al contrario, debería de ser el pan de cada día.

Además nos aporta la posibilidad de realizar modificaciones con más libertad sobre nuestros servicios internos, sabiendo que no afectará a todos los clientes finales (Al menos no directamente…), puesto que desacoplamos a los clientes de cómo nuestros servicios funcionan internamente. Es verdad que crear estas APIs adicionales es un esfuerzo extra que hay que mantener, y un punto de entrada y posible fallo en nuestro sistema, pero vuelvo a repetir, en una arquitectura de éste tipo existen herramientas para tracing (Jaeger) de las peticiones a lo largo de nuestros sistemas y es más, existen otras herramientas como Ocelot (Será el que usemos próximamente para el patrón API Gateway) pensadas específicamente para éste tipo de situaciones.

Con esto me despido por ahora! Espero que os haya servido éste patrón o que os sirva en un futuro no muy lejano… ¡¡Hasta el próximo artículo!!