Criando um BFF usando Shelf e Dart

Tempo de leitura: 6 minutes

Introdução

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.

Mas o que é um BFF?

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.

Os pontos positivos e negativos de usar um BFF

Positivos

  • 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

  • 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 Criar um 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.

Criando um BFF

Nesse tutorial vamos criar 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:

dart create -t server-shelf my_web_app

No arquivo server.dart vamos colocar dessa forma:

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_routerpara usar suas classes.

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 postdelete e etc.

 

Hello World!

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

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']}!");
  }
}

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:

{
  "name":"Jorge"
}

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:

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:

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:

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:

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. Ficou alguma dúvida ou sugestão, só deixar nos comentários abaixo. Até a próxima!