Construindo um BFF usando Shelf e Dart
Tags: dart, tutorial, shelf, flutter
Published: 2023-01-30 00:02:03

##Introdução
</br>
As aplicações mobiles modernas cresceram bastante em funcionalidades e, por consequência, em complexidade. A mesma aplicação pode consumir 8 a 10 serviços para exibir a tela inicial do app. Como manter uma aplicação coesa e de fácil manutenção diante desse cenário? Dai nasce o **Backend for Frontend** ou **BFF**.
</br>
### Mas o que é um BFF?
</br>
Um **Backend for Frontend** (BFF) é uma arquitetura de software que consiste em um ou mais serviços de back-end que são projetados especificamente para atender às necessidades de um aplicativo ou conjunto de aplicativos de front-end. O BFF age como uma camada intermediária entre o aplicativo de front-end e os serviços de back-end existentes, fornecendo uma interface de API personalizada e adaptada para atender às necessidades específicas do aplicativo de front-end.
</br>
![Exemplo gráfico de um BFF](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ycktaelbl1buzjqjhbx8.png)
</br>

###Os pontos positivos e negativos de usar um BFF
</br>
**Positivos**
</br>
- Permite aos desenvolvedores de aplicativos front-end se concentrar nas necessidades do usuário, enquanto o BFF se preocupa com as necessidades de comunicação com os serviços de back-end
- Possibilidade de criar uma interface de API personalizada e adaptada para o aplicativo de front-end, o que pode melhorar a performance e a escalabilidade do aplicativo
- Permite a separação de preocupações entre os desenvolvedores de front-end e back-end, o que pode melhorar a colaboração e a eficiência do time.

**Negativos**
</br>
- Adiciona uma camada adicional de complexidade à arquitetura do sistema
- Pode haver uma sobrecarga de trabalho adicional para os desenvolvedores, já que eles precisam manter e atualizar tanto o aplicativo de front-end quanto o BFF
- Pode exigir mais recursos computacionais para executar o BFF, o que pode afetar a escalabilidade e o custo do sistema.

##Casos de Uso

Para saber se essa arquitetura é suficiente para você, abaixo temos alguns casos de uso que podem ajudar na tomada de decisão:

- Comunicação com vários serviços de back-end: Quando um aplicativo de front-end precisa se comunicar com vários serviços de back-end diferentes, um BFF pode ser usado para agregar essas chamadas em uma única interface de API personalizada. Isso pode melhorar a performance e a escalabilidade do aplicativo.
- Segurança: O BFF pode ser usado para adicionar medidas de segurança, como autenticação e validação de entrada, antes de passar as solicitações para os serviços de back-end.
- Caching: O BFF pode ser usado para armazenar dados em cache e reduzir a frequência de chamadas para os serviços de back-end, o que pode melhorar a performance e a escalabilidade do aplicativo.
- Transformação de dados: O BFF pode ser usado para transformar os dados retornados pelos serviços de back-end antes de passá-los para o aplicativo de front-end, para que ele possa ser facilmente consumido pelo aplicativo.
- Gerenciamento de versão: O BFF pode ser usado para gerenciar diferentes versões de um aplicativo de front-end, permitindo que os desenvolvedores façam mudanças nas APIs sem afetar os aplicativos existentes.
- Aplicativos móveis ou de outras plataformas: O BFF pode ser usado para fornecer uma interface de API personalizada para aplicativos móveis ou de outras plataformas, como aplicativos de assistente de voz, para que eles possam se comunicar com os serviços de back-end.

##O Shelf

O Shelf é um package para Dart que fornece uma maneira fácil de criar servidores HTTP. Ele fornece uma interface de programação de aplicativos (API) limpa e intuitiva para lidar com requisições e respostas HTTP e permite que os desenvolvedores adicionem middlewares para suportar funcionalidades específicas, como autenticação, validação de entrada, etc.

O Shelf foi projetado para ser simples de usar e escalar facilmente, permitindo que os desenvolvedores possam lidar com cargas de trabalho altas sem prejudicar a performance do servidor. Ele também é fácil de integrar com outros pacotes e bibliotecas do Dart, o que significa que os desenvolvedores podem aproveitar as funcionalidades existentes e aumentar a produtividade.

##Shelf para Construção de BFF

Existem várias razões pelas quais o Shelf pode ser uma boa escolha para construir um Backend for Frontend (BFF) usando Dart. Algumas delas incluem:

- **Simplicidade**: O Shelf é projetado para ser simples de usar e possui uma interface de programação de aplicativos (API) limpa e intuitiva. Isso significa que os desenvolvedores podem se concentrar nas funcionalidades do BFF, em vez de lidar com a complexidade de baixo nível da criação de um servidor HTTP.
- **Flexibilidade**: O Shelf é altamente flexível e permite aos desenvolvedores adicionar middlewares para suportar funcionalidades específicas, como autenticação, validação de entrada, etc.
- **Escalabilidade**: O Shelf é projetado para escalar facilmente, permitindo que os desenvolvedores possam lidar com cargas de trabalho altas sem prejudicar a performance do BFF.
- **Integração**: O Shelf é fácil de integrar com outros pacotes e bibliotecas do Dart, o que significa que os desenvolvedores podem aproveitar as funcionalidades existentes e aumentar a produtividade.
- **Comunidade**: O Dart é uma linguagem de programação crescente e tem uma comunidade ativa, o que significa que os desenvolvedores podem obter ajuda e suporte rapidamente quando precisarem.

Considerando esses pontos, o Shelf é uma boa opção para construir um BFF usando Dart, pois ele oferece uma combinação de simplicidade, flexibilidade, escalabilidade e integração com outros pacotes e bibliotecas. Além disso, a comunidade ativa do Dart, garante suporte e suporte para os desenvolvedores.

## Construindo o BFF

Nesse tutorial vamos construir um BFF que retorna a idade, o gênero, o país de origem e uma proposta de atividade baseada em um nome.

Para construir o BFF utilizando Dart e Shelf vamos primeiro criar o projeto:

```shell
dart create -t server-shelf my_web_app
```
No arquivo `server.dart` vamos colocar dessa forma:

```dart
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';

import 'app/routes/routes.dart';

void main(List<String> args) async {
  // Use qualquer host disponível ou IP de contêiner (geralmente `0.0.0.0`).
  final ip = InternetAddress.anyIPv4;

  // Configure um pipeline que registra solicitações.
  final handler = Pipeline()
      .addMiddleware(
        logRequests(),
      )
      .addHandler(
        Routes.routes(),
      );

// Para execução em containers, respeitamos a variável de ambiente PORT.
  final port = int.parse(Platform.environment['PORT'] ?? '8080');
  final server = await serve(handler, ip, port);
  print('Server listening on port ${server.port}');
}

```
Vamos agora criar uma pasta `app` no mesmo nível do arquivo `server.dart` e nele criar as seguintes pastas:

```
app
|-controllers
|-routers
|-models
```
Dessa forma vamos organizar cada parte do nosso BFF.

###### Routers

É no arquivo de `routers.dart` que vamos dizer a nossa aplicação quais rotas vamos receber e qual o controller responsável por ela. Esse arquivo usa o package `shelf_router`para usar suas classes.

```dart
import 'package:shelf_router/shelf_router.dart' as shelf_router;

import '../controllers/hello_world_controller.dart';
import '../controllers/name_details_controller.dart';

class Routes {
  static shelf_router.Router routes() {
    final router = shelf_router.Router()
      ..get(
        '/helloworld',
        HelloWorldController.helloWorldHandler,
      )
      ..get(
        '/namedetails',
        NameDetailsController.nameDetailsController,
      );
    return router;
  }
}
```
Iremos colocar uma rota "Hello World!" e a outra para a rota que irá retornar as informações dos nomes que iremos passar por parâmetro. O método da rota é definido pela função `..get()`, caso queira outros adiciona-se `post`, `delete` e etc.

### Hello World!

Nada mais justo do que começar com um `Hello World`, e o controller fica muito simples:

```dart
import 'dart:convert';

import 'package:shelf/shelf.dart';

class HelloWorldController {
  static helloWorldHandler(Request request) async {
    Map<String, dynamic> response = json.decode(await request.readAsString());
    return Response.ok("Olá, ${response['name']}!");
  }
}
```
A `request` vem com todas as informações da requisição feita ao serviço. Nesse caso esperamos no body da requisição um json. Esse mesmo `json` que vamos usar no nosso `controller` dos nomes:

```json
{
  "name":"João"
}
``` 
O objeto `Response` é utilizado para determinar qual o _status code_ usado para o caso de sucesso e para tratamento de erro. O método `Response.ok()` retorna a string com um `status code` de `200`.

### Name Controller

Vamos à parte principal, o `name_controller.dart`. É nele que iremos fazer as requisições e tratamentos para juntar informações de vários servições em um só.

Primeiro, vamos estabelecer as chamadas a cada um dos servições que vamos utilizar:

```dart
    Map<String, dynamic> requestBody =
        json.decode(await request.readAsString());

    final urlAge = Uri.https('api.agify.io', '', {'name': requestBody['name']});

    final urlGender =
        Uri.https('api.genderize.io', '', {'name': requestBody['name']});

    final urlNationalize =
        Uri.https('api.nationalize.io', '', {'name': requestBody['name']});

    final urlActivy = Uri.https('boredapi.com', '/api/activity');

    final resultAge = await http.get(urlAge);
    if (resultAge.statusCode > 300) {
      return Response(resultAge.statusCode, body: resultAge.body);
    }

    final resultGender = await http.get(urlGender);
    if (resultGender.statusCode > 300) {
      return Response(resultGender.statusCode, body: resultGender.body);
    }

    final resultNationalize = await http.get(urlNationalize);
    if (resultNationalize.statusCode > 300) {
      return Response(resultNationalize.statusCode,
          body: resultNationalize.body);
    }

    final resultActivity = await http.get(urlActivy);
    if (resultActivity.statusCode > 300) {
      return Response(resultActivity.statusCode, body: resultActivity.body);
    }
```

Nessa primeira parte pegamos o `name` do body da requisição e pegamos as informações individualmente em cada serviço. Vamos, nesse primeiro momento, usar a estratégia "tudo ou nada": Se algum serviço falhar iremos retornar o erro desse serviço em específico, mas poderíamos tratar dentro de um `try catch` ou até criar uma resposta informando somente o erro, fica a cargo do leitor implementar isso para estudos.

Usamos o package "http" já amplamente utilizado para poder fazer as requisições aos serviços. 

Depois, para facilitar manipulações dos dados, vamos processar os responses em _Models_:

```dart
    final ageDecode = json.decode(resultAge.body);
    final genderDecode = json.decode(resultGender.body);
    final nationalizeDecode = json.decode(resultNationalize.body);
    final activityDecode = json.decode(resultActivity.body);

    final ageModel = AgeModel.fromJson(ageDecode);
    final genderModel = GenderModel.fromJson(genderDecode);
    final nationModel = NationModel.fromJson(nationalizeDecode);
    final activityModel = ActivityModel.fromJson(activityDecode);
```

E por fim, a montagem do nosso json de response e o envio para o cliente que solicitou:

```dart
    Map<String, dynamic> mapResponse = {
      'name': ageModel.name,
      'age': ageModel.age,
      'gender': genderModel.gender,
      'nationality': nationModel.country?[0].countryId,
      'suggestion_activity': activityModel.activity,
    };

    return Response(
      200,
      body: JsonEncoder.withIndent(' ').convert(mapResponse),
    );
```

Criamos o nosso _Map_ com as informações coletadas dos serviços e, ao colocar no boddy, precisamos processar esses dados em um `json` no formato adequado. E pronto, feito nosso processo. Nosso arquivo completo ficaria assim:

```dart
import 'dart:convert';

import 'package:shelf/shelf.dart';
import 'package:http/http.dart' as http;

import '../models/activity_model.dart';
import '../models/age_model.dart';
import '../models/gender_model.dart';
import '../models/nation_model.dart';

class NameDetailsController {
  static nameDetailsController(Request request) async {
    Map<String, dynamic> requestBody =
        json.decode(await request.readAsString());
    // final allParams = request.requestedUri.queryParameters;

    final urlAge = Uri.https('api.agify.io', '', {'name': requestBody['name']});

    final urlGender =
        Uri.https('api.genderize.io', '', {'name': requestBody['name']});

    final urlNationalize =
        Uri.https('api.nationalize.io', '', {'name': requestBody['name']});

    final urlActivy = Uri.https('boredapi.com', '/api/activity');

    final resultAge = await http.get(urlAge);
    if (resultAge.statusCode > 300) {
      return Response(resultAge.statusCode, body: resultAge.body);
    }

    final resultGender = await http.get(urlGender);
    if (resultGender.statusCode > 300) {
      return Response(resultGender.statusCode, body: resultGender.body);
    }

    final resultNationalize = await http.get(urlNationalize);
    if (resultNationalize.statusCode > 300) {
      return Response(resultNationalize.statusCode,
          body: resultNationalize.body);
    }

    final resultActivity = await http.get(urlActivy);
    if (resultActivity.statusCode > 300) {
      return Response(resultActivity.statusCode, body: resultActivity.body);
    }

    final ageDecode = json.decode(resultAge.body);
    final genderDecode = json.decode(resultGender.body);
    final nationalizeDecode = json.decode(resultNationalize.body);
    final activityDecode = json.decode(resultActivity.body);

    final ageModel = AgeModel.fromJson(ageDecode);
    final genderModel = GenderModel.fromJson(genderDecode);
    final nationModel = NationModel.fromJson(nationalizeDecode);
    final activityModel = ActivityModel.fromJson(activityDecode);

    Map<String, dynamic> mapResponse = {
      'name': ageModel.name,
      'age': ageModel.age,
      'gender': genderModel.gender,
      'nationality': nationModel.country?[0].countryId,
      'suggestion_activity': activityModel.activity,
    };

    return Response(
      200,
      body: JsonEncoder.withIndent(' ').convert(mapResponse),
    );
  }
}
```

E assim finalizamos nosso artigo! O projeto completo está [neste repositório](https://github.com/yagoliveira92/meetup-bff). Ficou alguma dúvida ou sugestão, só deixar nos comentários abaixo. Até a próxima!