No nosso último artigo, que pode ser lido aqui, tratamos um pouco das motivações para iniciar o estudo de backend com Dart e subimos um serviço teste utilizando do Aqueduct.

Nesse artigo vamos entender como o Aqueduct estrutura a aplicação e melhorar um pouco nosso primeiro serviço desenvolvido no último artigo.

Quando criamos nosso projeto usando o comando $ aqueduct create, o cli do Aqueduct já cria toda a estrutura necessária para rodar nosso projeto, como podemos ver na figura.



Temos dois pontos de atenção nessa estrutura inicial, o arquivo pubspec.yaml e a pasta lib.

É no arquivo pubspec.yaml que configuramos nosso projeto e adicionamos novas bibliotecas que serão utilizadas no projeto e inicialmente contem as seguintes informações.

name: flutter_course_server
description: An empty Aqueduct application.
version: 0.0.1
author: stable|kernel <jobs@stablekernel.com>

environment:
  sdk: ">=2.0.0 <3.0.0"

dependencies:
  aqueduct: ^3.0.0

dev_dependencies:
  test: ^1.0.0
  aqueduct_test: ^1.0.0



Como podemos observar na seção de enviroment podemos colocar a versão do SDK que vamos utilizar para fazer o build do projeto, nas seções dependencies e dev_dependencies podemos adicionar bibliotecas. As bibliotecas que vamos utilizar apenas no ambiente de desenvolvimento como por exemplo as bibliotecas de teste ou geradores de códigos, adicionamos como dependência de desenvolvimento.

Quando falamos de bibliotecas é importante ressaltar que o dart possui várias bibliotecas desenvolvidas pelas comunidade e podemos ver boa parte delas diretamente pelo site https://pub.dev/packages.

Outro ponto importante que devemos observar nesse arquivo é a extensão yaml. O YAML é um padrão de serialização de dados para todas as linguagens de programação que pode ser interpretada facilmente por humanos. Um ponto importante dos arquivos yaml é que a indentação dos campos descreve a hierarquia dos dados. Caso tenha curiosidade para entender melhor esse padrão, acesse as suas especificações em https://yaml.org/spec/1.2/spec.html.

Agora que já vimos o arquivo de configuração do projeto passamos para a importancia da pasta lib. É nessa pasta que ficará todo nosso código escrito em Dart. Basicamente é aqui que vamos desenvolver nossa aplicação.

Quando criamos um novo projeto com o Arqueduct, ele é inicializado com apenas um arquivo dentro da pasta lib, o channel.dart.

O channel é a origem da aplicação com o Aqueduct e tem um código bem simples:

class FlutterCourseServerChannel extends ApplicationChannel {
  
  @override
  Future prepare() async {
    logger.onRecord.listen((rec) => print("$rec ${rec.error ?? ""} ${rec.stackTrace ?? ""}"));
  }

  @override
  Controller get entryPoint {
    final router = Router();

    return router;
  }

}


Para o Arqueduct o channel é a entrada da aplicação, responsável por ouvir as requisições e enviar para o Router, que deverá redireciona-las para o recurso solicitado. Sendo assim, a aplicação só deverá ter um arquivo desse tipo.



Fazendo um paralelo do diagrama com nosso código conseguimos observar a ligação do nosso channel com o router, que é feito através da variável computada entryPoint, que deve devolver o router devidamente registrado.

Para registrar um novo controller no router basta adiciona-lo com o nome da rota e qual o controller associado. Em nosso primeiro artigo registramos uma nova rota para o recurso de ranking.

router
  .route('/ranking')
  .link(() => RankingController());



Nesse caso quando fizermos uma request para o endpoint /ranking o channel irá enviar para o router que deverá chamar nossa RankingController que deverá tratar a requisição.

Ainda olhando para nosso primeiro artigo, vamos observar como foi desenvolvido nosso controller

class RankingController extends Controller {
  final _ranking = [
    {'id': 1, 'name': 'Marcus', 'points': 123},
    {'id': 2, 'name': 'Fabio', 'points': 537},
    {'id': 3, 'name': 'Renato', 'points': 348},
    {'id': 4, 'name': 'Vinicius', 'points': 854},
    {'id': 5, 'name': 'André', 'points': 637},
  ];

  @override
  Future<RequestOrResponse> handle(Request request) async {
    return Response.ok(_ranking);
  }
}



Nossa classe RankingController extende a classe Controller do Aqueduct com a qual o router irá se comunicar, nesse caso através da função handle.

@override
Future<RequestOrResponse> handle(Request request) async {
  return Response.ok(_ranking);
}



Todas as requisição enviadas ao endpoint /ranking serão encaminhadas a essa função para ser tratada.

Para todos que já trabalharam na construção de aplicações backend, ao observar nosso método, se perguntam: Como vou tratar os verbos http e os parametros da minha request?

A resposta é simples, através da request que recebemos como parametro do método handle que contém todas as informações necessárias.

Somente como um exercício de exemplo, vamos supor que a request seja feita para fazer a requisição de um ranking específico, nesse caso o endpoint solicitado sera /ranking/1 para buscar o ranking com o ID igual a 1.

Podemos registrar nossa rota como:

route("/users/[:id]")



E no método handle na nossa controller podemos acessar essa variavel através do parametro request, nesse caso nossa classe ficaria da seguinte forma:

class RankingController extends Controller {
  final _ranking = [
    {'id': 1, 'name': 'Marcus', 'points': 123},
    {'id': 2, 'name': 'Fabio', 'points': 537},
    {'id': 3, 'name': 'Renato', 'points': 348},
    {'id': 4, 'name': 'Vinicius', 'points': 854},
    {'id': 5, 'name': 'André', 'points': 637},
  ];

  @override
  Future<RequestOrResponse> handle(Request request) async {
  	final identifier = request.path.variables["id"];

  	if (identifier != null) {
  	  final ranking = _ranking.firstWhere((element) => element['id'] == identifier);
  	  return Response.ok(ranking);
  	} else {
  	  return Response.ok(_ranking);
  	}
  }
}



Analisando o código acima é visível que essa estrutura é insustentável para manter, já que podemos ter diversos verbos http e parametros em nossas requests. Nesse caso a controller deve ser utilizada para endpoints mais simples, onde é necessário apenas a entrada da request para execução.

Para endpoints mais complexos, ou melhor dizendo, para tratar os nossos recursos o Aqueduct nos trás outra classe chamada ResourceController.

Transformando nossa Controller de ranking em uma ResourceController teremos o seguinte resultado.

class MeController extends ResourceController {
  final _ranking = [
    {'id': 1, 'name': 'Marcus', 'points': 123},
    {'id': 2, 'name': 'Fabio', 'points': 537},
    {'id': 3, 'name': 'Renato', 'points': 348},
    {'id': 4, 'name': 'Vinicius', 'points': 854},
    {'id': 5, 'name': 'André', 'points': 637},
  ];

  @Operation.get()
  Future<Response> getAll() async {
    return Response.ok(_ranking);
  }

  @Operation.get('id')
  Future<Response> getRankingById(@Bind.path('id') String identifier) async {
    final ranking = _ranking.firstWhere((element) => element['id'] == identifier);
    return Response.ok(ranking);
  }

}



Nesse caso ficaria mais simples dar manutenção em todos os endpoints de nossos recursos, basta trabalhar com a notação @Operation com o verbo desejado como:

Operation.post()
Operation.get()
Operation.put()
Operation.delete()



Passando como parâmetros os dados registrados em nossa rota, que no nosso exemplo era id.

route("/users/[:id]")



E podemos utilizar a notação @Bind para atribuir o valor enviado para um parametro de nossa função.

O Aqueduct nos fornece o bind para vários tipos de variáveis, conforme a tabela a baixo.



Nesse artigo conhecemos melhor como o Aqueduct estrutura nossa aplicação e como entregar nossos recursos de maneira organizada. Com base nesse estudo inicial já é possível entender como funciona uma aplicação robusta utilizando esse framework.



Links para referência:
Backend com dart: https://www.redspark.io/backend-com-dart/
Routing: https://aqueduct.io/docs/http/routing/
ResourceController: https://aqueduct.io/docs/http/resource_controller/