Rotas

As rotas HTTP são definidas por meio de módulos JavaScript que podem ser declarados na Virtual File System ou em pacotes JAZ.

No caso da Virtual File System, deve-se utilizar o diretório Configurações > Rotas HTTP e nele deverão ser criados os arquivos com as definições de rotas das APIs. Por padrão, as rotas serão avaliadas na ordem de definição, portanto é importante que os arquivos sejam prefixados com um número de 4 dígitos para indicar a prioridade daquela configuração em relação às demais, de forma similar ao que ocorre com os scripts de inicialização.

Para os pacotes JAZ, deve-se criar os arquivos no diretório routes na raiz do pacote. Todos os arquivos com extensão “.js” contidos nesse diretório serão tratados como definições de rotas. As rotas definidas em pacotes JAZ são carregadas depois da rotas da Virtual File System e em ordem indeterminada entre os pacotes JAZ instalados em uma base de dados. Na maioria das vezes, a ordem de pesquisa das rotas é irrelevante, pois as APIs definem rotas distintas. Caso a ordem de pesquisa de uma rota específica seja relevante, deve-se utilizar a propriedade RouteSetDef.prototype.order para definir a sua ordem em relação às demais. Para obter a relação de todas as rotas HTTP e suas ordens, pode-se utilizar o processo Desenvolvimento > APIs HTTP > Rotas.

O módulo que define as rotas de uma API deverá exportar um objeto ou um array de definições de rotas. Uma rota é uma associação entre um método HTTP, uma URL e um método de um controlador, responsável por processar a requisição e gerar a resposta dessa requisição.

Abaixo segue um exemplo de uma definição de um arquivo de rotas:

module.exports = {
  apiName: 'My API',
  apiHelp: 'API purpose.',
  basePath: '/api/mines/v1/',
  requiresAuth: true,
  controller: '@example/mines-api/controllers/example',
  scope: 'api.example',
  routes: [
    {
      method: 'GET',
      path: 'users',
      action: 'listUsers()'
    },
    {
      method: 'POST',
      path: 'users',
      scope: '-api.example.readOnly',
      action: 'createUser(request)'
    },
    {
      method: 'GET',
      path: 'users/key<number>',
      action: 'getUser(key)'
    },
    {
      method: ['POST', 'PUT', 'PATCH']
      path: 'users/:key<number>',
      scope: '-api.example.readOnly',
      action: 'updateUser(request, key)'
    },
    {
      method: ['DELETE']
      path: 'users/:key<number>',
      scope: '-api.example.readOnly',
      action: 'deleteUser(key)'
    },
    {
      method: 'GET',
      path: 'users/key<number>/groups',
      action: 'getUserGroups(key)'
    },
    {
      method: 'GET',
      path: 'users/key<number>/avatar',
      action: 'getUserAvatarImage(key)'
    }
  ]
};

Com base no exemplo, pode ser constatado que uma definição de uma API qualquer é declarada por meio de um objeto literal com as propriedades definidas por RouteSetDef.

Abaixo segue um resumo das propriedades:

  • apiName: indica que este conjunto de rotas está associado a API informada. Essa informação é importante para as ferramentas de documentação, que agrupam as rotas pelo nome da API.
  • apiHelp: descrição da finalidade da API.
  • basePath: caminho comum a todas as rotas definidas em routes. O valor informado será prefixado à URL de cada rota. O basePath é opcional, mas o seu simplifica a definição das rotas ao evitar a redundância de definir a URL completa em cada uma delas.
  • requiresAuth: determina se as rotas solicitadas exigem que o requisitante informe alguma autenticação válida. Caso a autenticação seja solicitada, o objeto session estará autenticado com o usuário indicado no token durante a execução da ação do controlador. Os tipos de tokens de autorização permitidos são o Basic e o Bearer. Eles devem ser informados pelo cabeçalho HTTP Authorization.
  • realm: indica a configuração e o isolamento dos ambientes JavaScript que devem ser utilizados para executar as ações das rotas. Por padrão, todas as rotas utilizam um pool comum de sessões Stateless do realm “http-router”. Defina um realm diferente do padrão quando observar que os recursos e scripts utilizados por um controlador serão muito diferentes do padrão de uso dos demais controladores ou quando for necessário um controle diferenciado do tempo de vida do ambiente JavaScript ou do tipo de ambiente (stateful ou stateless). Mais detalhes sobre a configuração de realms pode ser observado na classe RealmConfig.
  • controller: chave ou caminho do arquivo que contém o objeto controlador que executará as ações indicadas nas rotas.
  • scope: indica os escopos de autorização requeridos para a utilização desta API. Escopos definidos no conjunto de rotas se aplicam a todas as rotas definidas em routes.
  • routes: array com as definições das rotas. Uma rota é constituída do método HTTP que ela atende, a URL a ser tratada e o método a ser executado no objeto controlador. Mais detalhes de uma definição de rota em RouteDef. O array routes também poderá conter uma outra definição de RouteSetDef. Isso permite definir uma API mais complexa que define vários grupos de rotas associados a recursos distintos.

É recomendada que a URL dos recursos tenham o seguinte formato:

/api/<nome-api>/<versão>/<recurso>/<sub-rota>

Cujos elementos são:

  • nome-api: Definirá a URL base de todas as rotas da API. Evite nomes longos, pois é um texto enviado a cada requisição e dificulta a leitura de logs HTTP.
  • versão: versão da API (v1, v2, etc.). Deverá ser em minúsculo.
  • recurso: o nome do recurso. Ele deve estar formatado seguindo o recomendado pelo padrão REST, onde substantivos compostos são separados por hifens. Ex: titulos_a_vencer torna-se titulos-a-vencer e titulos_vencidos torna-se titulos-vencidos.
  • sub-rota: livre, normalmente uma identificação ou um sub-recurso. Exemplo: pedidos/:chave.

Uma rota poderá ser parametrizada, permitindo indicar que uma parte dela é variável e que o seu valor deve ser extraído e utilizado como um parâmetro do método do objeto controlador. Para isso, deverá ser utilizada uma das seguintes sintaxes:

  • :param_name: extrai o parâmetro até a localização do próximo separador “/” dentro da URL. Exemplo: a rota com URL “/api/classes/:id/def” interceptará uma requisição para “/api/classes/123456/def” e extrairá o parâmetro id com o valor ‘123456’. Todos os parâmetros extraídos por essa sintaxe são do tipo “string”.
  • :param_name<type>: extrai o parâmetro de forma similar a sintaxe anterior, mas o converte para o tipo informado. Se o tipo não puder ser convertido, será retornado um erro. Inicialmente, são suportados os tipos “number”, “date”, “string” e “boolean”.
  • *param_name: extrai o restante da URL e armazena o valor extraído em param_name. O nome do parâmetro pode ser suprimido, informando apenas *, quando deseja-se criar uma rota para o caminho informado e todos os sub-caminhos dele. Exemplo: a rota com URL “/api/files/*path” interceptará uma requisição para “/api/files/parent/file.js” e extrairá o parâmetro path com o valor ‘parent/file.js’.

A definição de rotas deve deve ser retornada por meio da propriedade module.exports, seguindo a convenção de módulos do CommonJS.

Recursos especializados x abstratos

Alcançar um alto nível de abstração é um objetivo frequente de arquitetos de APIs. Entretanto, uma grande abstração muitas vezes não é significativa para os desenvolvedores. Geralmente, classes e recursos abstratos tornam um sistema mais extensível, coeso e simples de manter, em detrimento da intuitividade da API para os desenvolvedores. Uma API que modela tudo no mais alto nível de abstração prejudica a capacidade do desenvolvedor de intuir quais funcionalidades o recurso possui. Por exemplo, os comportamentos esperados de um recurso “Cliente” são mais claros para um desenvolvedor dos que os de um recurso “Entidade”. Por outro lado, o mesmo recurso Entidade poderá ser utilizado para manipular todas as entidades do sistema, como “Fornecedores”, “Locais de Escrituração”, “Funcionários”, etc.

Via de regra, a estratégia recomendada na criação das primeiras APIs do sistema é a de criar uma API REST o mais similar possível as APIs já existentes nos objetos de gestão, da forma mais abstrata possível. O objetivo é liberar rapidamente o acesso a todas as capacidades do modelo de dados do sistema, mesmo que não seja da forma mais simples para os consumidores da API. Após essa primeira etapa, deverão ser construídas APIs mais especializadas e concretas para os casos em que haja benefício real de clareza ou desempenho.

Seguindo essa estratégia, o REST Framework disponibiliza todo o cadastro genérico por meio de uma API REST abstrata chamada Classes API. Os desenvolvedores dos módulos do sistema devem utilizar preferencialmente essa API, em vez de criar recursos especializados para os dados cadastrais. O foco deverá ser disponibilizar os dados de movimentações, acessados por meio dos objetos de gestão.

Versionamento

Quando uma API HTTP é disponibilizada para ser consumida por outras aplicações é estabelecido implicitamente um contrato entre dois sistemas. É responsabilidade do fornecedor do serviço não quebrar a compatibilidade da sua API.

No entanto, um software ativo está em constante evolução. Para possibilitar essa a evolução sem provocar prejuízos aos clientes das APIs, é recomendado que seja adotado um versionamento desde a primeira versão de uma API. Dessa forma, evoluções que sejam incompatíveis com os contratos já estabelecidos podem ser sinalizados por meio de uma nova versão da mesma API.

Existem várias estratégias de versionamento de APIs HTTP, mas aqui será apresentada apenas a convenção adotada pelas APIs fornecidas pela plataforma.

Convenção adotada pela plataforma

Uma API HTTP consiste de um conjunto de URLs, dos verbos HTTP suportados por elas, os parâmetros informados via URL, o formato esperado do corpo da requisição e o formato esperado do corpo da resposta. Esse conjunto de URLs e as ações associadas a elas são o que chamamos de roteamento e a essa definição deve ser dada uma versão. Para isso, por convenção se agrupa as URLs associadas a uma API sob uma mesma URL base acrescida de um número de versão. Abaixo segue um exemplo de rotas de uma API cuja URL base é “/api/operacoes” e está na versão 1.

/api/operacoes/v1/pedido/:id/cabecalho 
/api/operacoes/v1/pedido/:id/itens

Uma mudança de versão de uma API será realizada apenas quando uma melhoria ou correção exigir uma alteração de comportamento que impossibilite a manutenção do contrato anterior. A adição de novos comportamentos, em geral, não exigem a mudança de versão, pois as aplicações clientes que não fazem uso desses novos comportamentos não são afetadas.

Essa estratégia permite que o sistema evolua sua APIs de forma rápida, algumas vezes de forma transparente, para as aplicações clientes. No entanto, uma aplicação cliente que se utilize de recursos recém-disponíveis numa versão nova do sistema falhará ao tentar acessar uma outra base de dados que, apesar de publicar a mesma versão de API, não esteja com a última versão do sistema. É importante observar que os clientes usuários da plataforma não são prejudicados por esse comportamento ao adotarem uma política de atualização frequente.

Abaixo são dados alguns exemplos de alterações que não requerem uma nova versão:

  • Novos recursos
  • Novos métodos HTTP em recursos existentes
  • Novos formatos de dados suportados
  • Novos atributos e elementos em existentes tipos de dados

Por outro lado, os exemplos de alterações abaixo têm impacto e requerem uma nova versão:

  • URLs renomeadas ou removidas
  • Dados estruturalmente diferentes retornados por uma mesma URL
  • Removido suporte para um determinado método HTTP para uma URL existente

Quando de fato for necessário criar uma nova versão de uma API, é recomendado que ela mantenha a maior quantidade possível de comportamentos da versão anterior inalterados, reduzindo o esforço de revisão das aplicações existentes para utilizar a nova versão.

Versão da API e a evolução do modelo de dados

O modelo de dados do sistema pode ser alterado pelo fornecedor, parceiros ou pelos clientes. As modificações podem ser a criação de campos, a alteração dos existentes ou a exclusão deles. Também podem ser criadas, alteradas ou removidas validações e sugestões de valores nos eventos do modelo de dados. Essas mudanças podem ocorrer a cada versão do sistema, mas alterações dessas naturezas não irão alterar a versão das APIs HTTP disponibilizadas pela plataforma.

Normalmente os campos recém criados são opcionais, no entanto, algumas mudanças podem exigir a revisão dos clientes consumidores da API, como: criar campos obrigatórios, tornar campos somente para leitura, excluir campos utilizados pela API cliente, adicionar novas validações de regras de negócio ou integridade. Para todos esses casos será retornado um erro HTTP.

A princípio, esse é um comportamento indesejado, pois uma vez que a versão da API estabelece um contrato, ele deveria ser respeitado. No entanto, mudanças no modelo de dados, como a adição de campos requeridos, podem ser exigências de uma nova regra de negócio e não faria sentido permitir que as APIs introduzissem dados inconsistentes com as regras estabelecidas pelo negócio. Além disso, mudanças do modelo de dados podem fugir do controle do criador da API. Por exemplo, se um cliente ou um parceiro tornar um campo requerido por meio de uma customização, não seria possível o fornecedor do sistema evoluir a versão de uma API que faz parte do produto, visto que ele nem tomará conhecimento dessa alteração.

Estruturas mais voláteis, como as classes de dados, que podem alterar a cada versão do sistema, podem ser consultadas em tempo de execução por meio da API de Classes. Nela é possível consultar o esquema de dados de uma classe, de forma similar ao método ClassDefManager.getModelDef, possibilitando assim que APIs clientes sejam robustas para as evoluções do modelo de dados.

Por motivos que vão além das APIs HTTP, é importante que os desenvolvedores responsáveis pela definição do modelo de dados avaliem os impactos das mudanças, sempre que possível sugerindo padrões para os valores que antes não eram exigidos. Mudanças que produzam impactos aos usuários ou consumidores das APIs sempre devem ser documentadas por meio de publicações técnicas na liberação de uma nova versão do sistema.

Escopos de autorização

Escopos de autorização podem ser configurados para um conjunto de rotas ou para uma rota específica. É recomendado que sempre seja definido ao menos um escopo de autorização para um conjunto de rotas de uma API HTTP a fim de permitir que o administrador do sistema possa controlar o seu acesso de uma forma mais simples, podendo também ser definidos escopos de autorização mais específicos para determinadas rotas dessa API.

Apesar de ser possível definir escopos de autorização para controlar o acesso de cada rota definida por uma API, deve-se levar em consideração que o sistema também possui um controle mais geral e rico de permissões aplicado às classes de dados. Sempre que for possível, as permissões das classes de dados devem ser utilizadas como controle de acesso em vez dos escopos de autorização, pois as permissões configuradas se aplicam a todo o sistema, e não apenas às APIs HTTP. O uso de escopos de autorização é recomendado para o controle de privilégios que não são claramente vinculados às classes de dados do sistema. Por esse motivo, não é incomum que uma API HTTP possua apenas um único escopo de autorização definido e o controle de acesso aos sub-recursos seja realizado exclusivamente pelo modelo de permissões.

Escopos de autorização são configurados por meio das propriedades RouteSetDef.prototype.scope e RouteDef.prototype.scope nas definições das rotas. Deve ser informada uma lista separada por espaço dos nomes dos escopos de autorização necessários para utilizar uma rota ou um conjunto de rotas. Opcionalmente, pode ser informado um array com os escopos, o qual será convertido em uma lista.

Via de regra, o acesso a uma rota será permitido se os escopos autorizados pelas credenciais utilizadas na autenticação do usuário contiverem ao menos um dos escopos informados nessas propriedades. Essa regra padrão pode ser alterada adicionando os prefixos ‘+’ e ‘!’ aos escopos informados. O prefixo ‘+’ tornará o escopo obrigatório e ele sempre deverá estar relacionado nos escopos associados às credenciais. Já o prefixo ‘!’ indica que a rota não será autorizada se o escopo for encontrado. No exemplo no início deste documento, foi configurado o escopo '-api.example.readOnly' para indicar que as rotas que alteram dados não podem ser utilizadas se o escopo api.example.readOnly for atribuído ao usuário ou ao token de autorização utilizado na autenticação da requisição.

É requerido que todos escopos de autorização utilizados nas rotas das APIs HTTP sejam cadastrados em Admin > Segurança > Escopos de autorização a fim de documentar o seu propósito e permitir que o administrador possa atribuir esses escopos aos usuários, grupos e papéis do sistema.

APIs que tenham a propriedade apiName e scope definidas poderão ser autorizadas diretamente pelo administrador do sistema por meio do processo Admin > Segurança > Tokens de autorização.