Controladores

Controladores são objetos JavaScript cujos métodos são responsáveis por tratar os dados de uma requisição HTTP, consultar ou alterar os dados necessários e retornar um resultado por meio de uma resposta adequadamente formatada. Qualquer objeto JavaScript pode ser um controlador, mas por conveniência são utilizadas classes filhas de Controller.

Para explicar os conceitos envolvidos na criação de um controlador, serão apresentados exemplos da definição de um controlador e de suas rotas, onde serão destacados alguns pontos que serão abordados em seguida com mais detalhes.

Definindo um controlador HTTP

/products/Custom/controllers/UserControllers.js (-1895835240)

const Controller = require('@nginstack/engine/lib/router/Controller');
const Entity = require('@nginstack/orm/lib/Entity');
const EntitySet = require('@nginstack/orm/lib/EntitySet');
const ClassKeys = require('@nginstack/engine/keys/Classes');
const inherits = require('@nginstack/engine/lib/object/inherits');

function UserController() {
  Controller.call(this);
  this.users_ = EntitySet.fromClass(ClassKeys.USERS); // (1) EntitySet representa as entidades de uma classe de dados
};
inherits(UserController, Controller); // (2) É recomendado que os controladores herdem de Controller

UserController.prototype.listUsers = function () {
  return this.ok(this.users_); // (3) A classe Controller tem métodos para as respostas HTTP mais frequentes
};

UserController.prototype.getUser = function (userKey) {
  const entity = this.users_.findByKey(userKey);
  if (entity) {
    return this.ok(entity);
  } else {
    // (4) Erros em uma API HTTP devem ser sinalizados por estados adequados, como o 404 NOT FOUND 
    return this.notFound(new Error('Não foi possível encontrar um usuário com a chave ' + userKey));
  }
};

UserController.prototype.updateUser = function (request, userKey) {
  const entity = this.users_.findByKey(userKey);
  if (entity) {
    entity.assign(request.body.asJson()); // (5) O corpo da requisição normalmente será um JSON
    entity.post();
    return this.ok(entity);
  } else {
    // (6) Erros também podem ter seus estados sinalizados por meio da classe HttpError  
    throw new HttpError.NotFound('Não foi possível encontrar um usuário com a chave ' + userKey);
  }
};

// (7) O retorno de um módulo de definição de um controlador deve ser o construtor dele
module.exports = UserController;

/Configurações/Rotas HTTP/9000 Users API.js

module.exports = {
  apiName: 'Custom Users API',
  basePath: '/api/users/v1/',
  controller: -1895835240, // UserControllers.js
  requiresAuth: true, // (8) APIs que manipulem dados devem exigir autenticação do usuário
  routes: [
    {
      method: 'GET',
      path: 'users',
      action: 'listUsers()' // (9) Uma ação definida em uma rota deve ser um método do controlador
    },
    {
      method: 'GET',
      path: ':users/key<number>',
      action: 'getUser(request, key)' // (10) Parâmetros podem ser extraídos da URL
    },
    {
      method: ['POST', 'PUT', 'PATCH'] // (11) Uma rota pode tratar múltiplos métodos HTTP
      path: 'users/:key<number>',
      action: 'updateUser(request, key)'
    }
  ]
};

Alguns pontos destacados dos exemplos:

  1. O REST Framework disponibiliza as classes EntitySet e Entity com o objetivo de simplificar a manipulação dos registros das classes de dados. Mais detalhes dessas classes serão apresentados em Manipulação dos Dados. O uso dessas classes não é obrigatório, sendo possível acessar os dados utilizando os objetos de gestão ou acessando diretamente os registros das tabelas por meio das APIs DBCache e Database.
  2. Apesar de não ser obrigatório, é recomendado que os controladores herdem da classe Controller. Ela possui métodos que simplificam o tratamento da requisição e a geração das respostas.
  3. O retorno de uma ação de um controlador deve ser uma instância de RouteResult. A classe Controller disponibiliza métodos como ok, notFound, created e badRequest para as respostas mais comuns de uma API HTTP. Nada impede no entanto que um controlador crie uma instância de RouteResult diretamente para ter um maior controle sobre o retorno da API.
  4. Os códigos de status HTTP devem ser utilizados para indicar a natureza do erro e o código 200 OK deve ser restrito para operações bem sucedidas. É comum que clientes HTTP tratem os status 4xx e 5xx como erro, portanto fazer uso desses códigos simplifica o uso da API.
  5. Em APIs HTTP REST, o corpo da requisição normalmente será um objeto codificado em JSON. Isso no entanto não é obrigatório e uma API HTTP pode receber ou responder XML, conteúdos binários ou qualquer outro tipo de dado.
  6. Os códigos de status HTTP também podem ser definidos no resultado usando a classe de erros HttpError. O resultado gerado por esse erro tem o código de status automaticamente transformado pelo REST Framework para o código de erro que for enviado por parâmetro no seu construtor. Para os códigos de status mais recorrentes, também é possível usar as classes herdeiras específicas como HttpError.BadRequest, HttpError.Forbidden, HttpError.NotFound e HttpError.Unauthorized.
  7. O módulo que define um controlador deve exportar um construtor. A criação da instância de um controlador é realizada pelo REST Framework sob demanda. Inicializações que tenham um custo significativo devem preferencialmente ocorrer no construtor e construções de objetos devem ser evitadas no escopo do módulo. Um controlador sempre deve ser stateless e não deve fazer cache de informações que sejam relacionadas ao usuário requisitante. Uma vez criado, um controlador é guardado em um cache pelo REST Framework e uma mesma instância do controlador poderá ser utilizada para atender requisições de diversos usuários.
  8. APIs que acessem a base de dados devem solicitar a autenticação do usuário. O Engine não permite alterações na base de dados a partir de sessões anônimas.
  9. Na definição de uma rota, uma ação é representada como a chamada de um método do controlador. A definição de uma rota falhará se o método indicado não existir no controlador.
  10. Em uma ação podem ser informados os argumentos que devem ser passados para o método do controlador. Os argumentos podem ser valores fixos, parâmetros extraídos da URL ou as variáveis globais request e response, que representam a requisição e a resposta HTTP. Veja mais detalhes em RouteDef.
  11. Uma mesma rota pode tratar vários métodos HTTP. Nesses casos, a propriedade Request.method pode ser utilizada no controlador para identificar o método que de fato está sendo atendido.

Respostas com status HTTP significativo

O protocolo HTTP tem o conceito de códigos de estados de resposta para dar uma visão geral de como a requisição foi atendida, sem haver a necessidade de inspecionar o conteúdo da resposta para esse fim.

O uso adequado dos códigos de estado HTTP simplificam o consumo da API, pois permitem que as aplicações de terceiro direcionem a resposta para o tratamento adequado sem a necessidade de processá-la. Por exemplo, uma interface Web que esteja consumindo uma API HTTP pode interceptar todas as requisições com código 4xx ou 5xx para uma rotina que apresente um diálogo de erro para o usuário. Também é comum que as aplicações interceptem o status 401 Forbidden para direcionar o usuário para uma tela de login.

É responsabilidade do controlador indicar o código do status do resultado, o que normalmente é feito de forma implícita pelos métodos de construção de resultados da classe Controller ou de forma explícita pelo método RouteResult.withStatus.

Os códigos HTTP são divididos em:

  • 1xx: informacionais
  • 2xx: bem sucedidos
  • 3xx: redirecionamento
  • 4xx: erros de cliente
  • 5xx: erros de servidor

Os códigos de estados mais utilizados são:

  • 200 - OK: mensagem geral para indicar um sucesso
  • 201 - Created: sucesso, normalmente utilizando na criação de recursos
  • 202 - Accepted: usado quando uma API irá processar a requisição de forma assíncrona
  • 204 - No Content: sucesso, normalmente utilizado quando um recurso é excluído
  • 400 - Bad Request: dados enviados pelo clientes são incompletos ou inválidos
  • 401 - Unauthorized: o usuário não foi autenticado
  • 403 - Forbidden: o usuário foi autenticado, mas não tem permissão de acesso ao dado
  • 404 - Not Found: o recurso solicitado não existe
  • 405 - Method Not Allowed: indica que método HTTP não é suportado para o recurso
  • 500 - Internal Server Error: erro do servidor não tratado pelo objeto controlador

Filtros via query string

A query string é um tipo de parametrização adicional e opcional, que não precisa estar declarada no arquivo de rotas e nem na assinatura da ação. Ela é informada na URL e é tratada pelo objeto request do Engine, sendo os seus valores acessíveis na propriedade Request.params. Exemplo de uma URL com query string:

http://127.0.0.1:81/api/user/v1/user?age=18&name=Atom

Rotas de listagem de dados devem preferencialmente permitir que o usuário filtre as entidades desejadas. O uso da query string é a forma mais natural e prática de receber os filtros sobre uma coleção de dados.

Tratamento de erros

Durante o atendimento de uma requisição HTTP há dois tipos de exceções no que se refere ao tratamento realizado pelo REST Framework:

  • exceções capturadas e tratadas, com retornos definidos pelo controlador: Recomendada para os casos em que o erro não é criado na lógica do controlador, e precisa ser capturado e tratado. Neste caso, é da responsabilidade dele especificar um retorno contendo o status HTTP, o código de erro e uma mensagem compreensiva a fim de ser exibida para o requisitante. Para os status mais comuns, as respostas podem ser geradas usando os métodos da classe Controller.
  • exceções diretamente lançadas sem captura ou definição de um retorno: Tais lançamentos são automaticamente encapsulados em um objeto de erro pelo REST Framework e por padrão são retornados com o código de status 500 (Internal Server Error). Esse comportamento pode ser alterado por meio de transformações automáticas de resultados, explicado com mais detalhes em Manipulação dos dados.

Para simplificar o uso controlado deste tipo de tratamento de exceção, utiliza-se a classe de erros HttpError. Este uso é recomendado quando há a necessidade de que a lógica do próprio controlador lance um erro, e é um facilitador especialmente quando o lançamento deste erro é delegado a funções auxiliares, como funções de validação e checagem por exemplo.

Para não expor informações potencialmente privilegiadas para os usuários da API, alguns detalhes dos erros, como a pilha de chamadas, são gravados apenas no log do Engine e são identificados no log por um número único chamado de ticket. Esse número de ticket será enviado na resposta, permitindo que o requisitante da API possa consultar mais detalhes do erro caso tenha acesso ao log do Engine.

O requisitante identificará que ocorreu um erro durante a execução da API ao receber uma resposta com um código 4xx ou 5xx. O corpo dessa resposta conterá um JSON com mais detalhes do erro, tendo no mínimo as propriedades name e message. Classes de erros mais especializadas, como o DetailedError e o HttpError, poderão adicionar outras propriedades como details, code e solution. Mensagens capturadas pelo REST Framework que não foram tratadas pelo controlador também terão a propriedade ticket, contendo o identificador a ser pesquisado no log do Engine para obter mais detalhes técnicos sobre o erro.

Os códigos de erro de uma API HTTP dividem-se em dois grupos: os erros 4xx, usados para indicar que foi uma falha na requisição do cliente, e os erros 5xx, que indicam uma falha no servidor. Quando um cliente recebe como retorno um erro 5xx, ele poderá tentar enviar novamente a requisição sem alterações, pois a causa do problema é um erro no servidor que, a princípio, não tem relação com a requisição enviada. Já um erro 4xx indica para o cliente que a requisição possui um erro que deve ser corrigido antes de uma nova tentativa.

Rastreamento por meio de logs

É recomendado que o objeto controlador utilize a API Logger para registrar situações incomuns, fazendo uso das prioridades de log abaixo de forma adequada:

  • info: informações que identifiquem operações importantes do sistema, como aprovar pedido, baixar pedido, adicionar usuário, entre outras.
  • warn: um aviso que identifique que algo não esperado ocorreu, mas será possível continuar. Uma outra aplicação é quando o comportamento identificado poderá provocar uma falha futura.
  • error: um erro que fez com que a requisição tivesse que ser abortada. Não deve haver processamento após um log de erro.

Os arquivos de log são removidos automaticamente pelo Engine, portanto os registros das situações incomuns que precisem ser preservadas após o período de retenção dos logs em disco devem ser gravados na base de dados.

Convenção de diretórios

Os módulos que definem os controladores podem estar em qualquer diretório da Virtual File System ou da Union File System, mas por uma questão de padronização, são adotadas as seguintes convenções:

  • Virtual File System: /products/<product-name>/controllers/<ControllerName>.js
  • Union File System: /packages/<package-name>/controllers/<ControllerName>.js

Eventos emitidos durante o atendimento de uma requisição HTTP

A classe Controller emite o evento beforeAction antes de executar o método associado a uma ação de uma rota HTTP. Esse evento é oportuno para realizar validações comuns a todas as ações implementadas pelo controlador.

this.on('beforeAction', function (evt) {
  const data = evt.request.body.asJson();
  if (!data.user) {
    throw new HttpError.BadRequest('User not informed.');
  }
});

Se durante a execução do método associado a uma ação de uma rota HTTP for lançado um erro, será emitido o evento error. Seu uso é recomendado para implementar regras que somente devem ocorrer em situações de erro, como a criação de registros de log ou a geração de tickets de rastreamento do erro.

this.on('error', function (evt) {
  logger.error('Error gerado pelo controlador Test: ' + evt.error.message);
});

Após execução do método associado a uma ação de uma rota HTTP é emitido o evento afterAction. Este evento é adequado para transformar o resultado gerado pelos métodos do controlador em um formato específico esperado pelo cliente da API HTTP ou para a configuração dos cabeçalhos da resposta HTTP que será enviada para o cliente.

this.on('afterAction', function (evt) {
  const original = evt.result;
  route.result = RouteResult()
    .withStatus(Status.OK)
    .withContent({
      success: !evt.error
      data: JSON.stringify(original.content)
    });    
  evt.response.setHeader('x-ticket', ticketId);
});

Os listeners desses eventos receberão uma instância de ControllerEvent. Veja a documentação desta classe para mais detalhes das propriedades disponíveis.