Endpoints: o caminho por onde todas as requisições passam
Fala, galera! Quem mora em condomínio sabe que toda e qualquer encomenda que chegue para os condôminos tem que passar pela portaria. Mas o que isso tem a ver com programação? Essa é uma simples analogia sobre o tema deste artigo: endpoints Phoenix.
Os endpoints são o lugar por onde todas as requisições passam em todas as aplicações web, ou seja, funcionam como a portaria do seu prédio: tudo passa por ela antes de chegar na sua casa.
Aqui pretendo falar do ponto de vista técnico sobre o que acontece com o Phoenix, do momento em que recebe uma requisição até a resposta para ela.
Plug-se
É muito difícil falar sobre Phoenix sem abordar o Plug. Quem já trabalha com o Phoenix sabe que eles são bastante ligados, afinal, todo o framework foi construído em cima do Plug. Inclusive, posso garantir a vocês, não é como um filme de terror, mas: “We see Plug… all the time”.
Phoenix é um framework que basicamente dá ferramentas muito boas para você conseguir trabalhar com o Plug puro, que é um ‘pouquinho’ mais complicado.
Assim, os módulos Endpoint, Router e Controllers são uma mão na roda para fazer o que você precisa fazer. E isso só muda quando você começa a mexer com channels e sockets. Mas não posso dar mais detalhes sobre isso porque ainda não fiz essa experiência.
A estrutura básica do Plug
Para mostrar como funciona uma aplicação web Phoenix, vou mostrar a estrutura básica do Plug. A primeira coisa que você precisa saber é que trata-se de um módulo simples, com duas funções.
Não precisava colocar esse behaviour aqui. Só preciso desse init/1 e desse call/2. E o que esse call/2 vai fazer? Basicamente só vai mexer com a estrutura que representa uma conexão, que é esse o primeiro argumento conn, ou seja, o %Plug.Conn{}. Aí ele fará a manipulação alterar essa struct e jogar para o próximo plug para que ele faça a parte dele.
Nesse caso aqui, como ele é bem simples, tudo que ele faz é trocar o verbo HTTP de HEAD para GET, alterando o campo method. Se der match, ele atualiza essa struct e só repassa para o próximo Plug.
A struct é basicamente essa do código abaixo. Ela representa a conexão e a ganhamos a partir do momento que o cliente manda alguma requisição para o nosso servidor.
Tem alguns campos importantes aqui. É interessante reparar que alguns deles estão marcados como unfetched. Nesse momento, você não tem um valor definido para eles, por essa razão que o valor deles é essa struct %Plug.Conn.Unfetched{}. Porém, o Endpoint do qual falo lá no título é este daqui:
É o caminho que toda requisição faz e que fará com que ele passe por alguns Plugs. No final, ele cairá para o nosso Router, que já terá rotas definidas.
Ele vai fazer o match da rota que está no path_info com essas strings depois das funções get e post. Através dessas strings que o router verá o que faz sentido e fará o parse dos path_params.
Vale falar também dessas funções. Existe uma função para cada verbo HTTP. Assim, mesmo que a rota “/” se repita, o Router não vai se confundir, pois além da rota em si ele olha para o verbo HTTP e sabe diferenciar qual match é correto para o caso.
Super-gêmeos, parsear!
As pessoas mais novas não devem lembrar, mas tempos atrás Hanna-Barbera, mago dos quadrinhos, criou os irmãos alienígenas que ativam e desativam o poder de se transformar em qualquer coisa. Bom, parsear é mais ou menos isso: transformar A em B.
Não à toa, existe um plug, o Plug.Parsers, que é justamente o cara responsável por ler a requisição e traduzir o campo body_params para um map. Além disso, ele também lê o campo query_string e coloca o mapa correspondente em outro campo, que é o query_params. Cada vez que algum plug faz o parse dos campos body_params, query_params e path_params ele vai dar um Map.merge/2 no campo params da struct %Plug.Conn{}.
Ou seja, aqui o params agrupa todos esses três tipos de parâmetros — path, query e body — e todos eles ficam disponíveis no campo params.
Aí se você precisar necessariamente saber se um parâmetro veio do path, query ou body você precisará conferir no campo query_params, path_params ou body_params, se for o caso.
Aqui é um código hipotético, mas ele basicamente explica o que o router faz. Não é mais do que isso: ver qual o controller deu match com a rota para que possa chamar a função que eu defini no Controller.
Agora vem a mágica: pegamos a struct conn e o params e aplicamos a lógica do nosso negócio em cima dessa requisição.
O Phoenix oferece outros recursos que são realmente muito bons de conhecer, mas os três dos quais falei são mais fundamentais se você quer entender bem e não se perder no Phoenix.
Os deveres do endpoint
O sua “portaria”, o endpoint, tem alguns deveres a cumprir. Vamos falar deles agora. Entre suas tarefas está colocar toda essa manipulação dentro de uma árvore de supervisão. Dessa forma, qualquer falha faz com que a árvore de supervisão faça algo sobre isso.
Esse módulo endpoint também define um pipeline inicial para todas as requests, ou seja, todas elas vão passar por ali. Ele aproveita e guarda algumas configurações. Porém, qualquer coisa além disso a tarefa deixa de ser do endpoint e passa a ser dos plugs.
Aí você me pergunta: o que é comum encontrar no endpoint? Bom, tem alguns plugs para lidar com CORS e CSRF, que são relativos a segurança. Questões de autenticação, também podem ser feitas, como validação de token. A lib — Guardian — faz isso: ela lê o seu token e verifica se ele é válido.
Também se faz coleta de métricas no endpoint. É muito comum encontrar plugs de algumas plataformas de monitoramento e de coleta de logs. Essas plataformas costumam fornecer um plug para você colocar lá no seu endpoint e já de uma forma fácil integrar a sua aplicação com essa plataforma. Exemplos disso são o DataDog e o NewRelic. Ambos têm plugs para facilitar essa integração.
Antes de finalizar, vale a pena comentar também sobre o Plug.RequestID. Trata-se de um plug muito simples, mas extremamente útil, principalmente quando você tem uma aplicação clusterizada, ou seja, tem vários nós que vão processar alguma parte da requisição.
A função dele é definir um ID para a sua request, normalmente na chave x-request-id no header da request, embora você possa trocá-la, se quiser. Ele vai jogar isso num metadado do Logger e quando fizer a requisição para algum outro nó ou algum outro serviço do seu cluster ou pod, vai lê-lo e não vai sobrescrevê-lo. Isso fará com que todos eles tenham essa mesma ID nos metadados. Assim, você consegue facilmente colocar o ID da requisição lá no seu kibana ou equivalente e conseguirá saber por onde essa requisição passou e cada um dos pods que tenha participado no processamento da request. Os logs estarão lá bonitinhos esperando por você para que saiba tudo o que aconteceu. Tem até um nome bonito: distributed traicing.
Para fechar a conta de vez, vamos falar um pouquinho do telemetry. Ele tem a função de coletar algumas métricas. A struct %Plug.Conn{} guarda uma lista de funções a serem executadas before_send à resposta. Em outras palavras: a requisição vai passar pelo plug Plug.Telemetry que vai coletar o horário da requisição e quando a resposta da request for entregue, a função do campo before_send verá a diferença de tempo e saberá quanto durou a requisição.
Todas as partes do Phoenix estão trabalhando com essa struct do plug, então entender isso é uma mão na roda quando você precisa fazer algo um pouco mais elaborado. Ajuda lembrar que, na verdade, por baixo, quem está trabalhando é o plug e não o Phoenix.