Vue.JS: consumindo REST API’s com Services

Vue.JS: consumindo REST API’s com Services

O desenvolvimento front-end tem se encarregado de consumir serviços providos pelo back-end de formas cada vez menos acopladas. Uma estratégia de consumo desses serviços é o REST. Vamos ver alguma coisa sobre o funcionamento do REST, sua relação com o protocolo HTTP e discutir possibilidades acerca do tema dentro do Javascript.

Recomendo ler este artigo aqui antes de prosseguir.

REST

Representational State Transfer, abreviado como REST, é um modelo proposto para se projetar arquiteturas de software distribuído, que utilizem comunicação em rede de alguma forma. Quando associado ao HTTP é comum usarmos os verbos HTTP combinados com URL’s bem organizadas para conseguir representar as transferências de estados proposta.

REST é sobre organização e não sobre uma tecnologia ou uma ferramenta.

Fonte: https://www.slideshare.net/restlet/the-neverending-rest-api-design-debateFonte: slideshare.net/restlet/the-neverending-rest..

Combinando a semântica dos verbos HTTP com a proposta de significado do REST chegamos à um quadro como o acima.

Service REST API

Para poder montar nossa composição vamos ter uma estrutura de pastas como a que está abaixo:

A estrutura física de diretórios é meramente ilustrativa. Organize como julgar mais conveniente, o mais importante é compreender as abstrações e suas aplicações

Primeira vamos criar o nosso “Service.js”, ele será basicamente um wrapper para options e poderá ser reusado em situações muito variadas.

/**
 * @typedef {Service}
 */
export default class Service {
  /**
   * @param {*} options
   */
  constructor (options) {
    this.options = options
  }

  /**
   * @param {*} options
   */
  static build (options) {
    return new this(options)
  }
}

Em seguida vamos criar nossa classe HTTP como um wrapper para o nosso HTTP Client. Vamos usar o axios, mas poderia ser qualquer biblioteca. Para termos uma flexibilidade o HTTP irá importar o “standard.js”, mas poderá receber outros clientes.

import axios from 'axios'

const standard = axios.create({
  baseURL: process.env.api.BASE_URL,
  timeout: 100000,
  transformResponse: [
    function (data) {
      return data
    }
  ]
})

// standard.interceptors.response.use(..., ...)

export default standard

Perceba que o “standard.js” acima está bem simples e que você pode expor funções para atualizar headers e modificar a instância do axios caso seja necessário. Veja também que o Http.js abaixo vai estender o Service e usar os métodos que o axios entrega para criar métodos base normalizados para realizar as requisições com base nos verbos HTTP.

import standard from 'src/config/service/standard'
import Service from 'Service'

/**
 * @typedef {Http}
 */
export default class Http extends Service {
  /**
   * @param {String} path
   * @param {Object} options
   * @param {Object} http
   */
  constructor (path, options = {}, http = null) {
    super(options)
    this.path = path
    this.http = http || standard
  }

  /**
   * @param {String} path
   * @param {Object} options
   */
  static build (path, options) {
    return new this(path, options)
  }

  /**
   * @param {String} url
   * @returns {*|Promise<any>}
   */
  get (url) {
    return this.http
      .get(this.constructor.normalize(this.path, url))
      .then(this.constructor.then)
  }

  /**
   * @param {String} url
   * @param {Object} data
   * @returns {*|Promise<any>}
   */
  post (url, data) {
    return this.http
      .post(this.constructor.normalize(this.path, url), data)
      .then(this.constructor.then)
  }

  /**
   * @param {String} url
   * @param {Object} data
   * @returns {*|Promise<any>}
   */
  put (url, data) {
    return this.http
      .put(this.constructor.normalize(this.path, url), data)
      .then(this.constructor.then)
  }

  /**
   * @param {String} url
   * @param {Object} data
   * @returns {*|Promise<any>}
   */
  patch (url, data) {
    return this.http
      .patch(this.constructor.normalize(this.path, url), data)
      .then(this.constructor.then)
  }

  /**
   * @param {String} url
   * @returns {*|Promise<any>}
   */
  delete (url) {
    return this.http
      .delete(this.constructor.normalize(this.path, url))
      .then(this.constructor.then)
  }

  /**
   * @param {Object} response
   * @returns {Object}
   */
  static then (response) {
    if (!response.data) {
      return {}
    }
    if (typeof response.data === 'string') {
      return JSON.parse(response.data)
    }
    return response.data
  }

  /**
   * @param {String} start
   * @param {String} end
   * @returns {String}
   */
  static normalize (start, end) {
    return `${start}/${end}`.replace(/([^:]\/)\/+/g, '$1')
  }
}

Com o Http criado podemos seguir para uma especialização do Http que é o Api, nesta parte da composição estamos abstraindo o ponto de montagem das rotas da API do back-end que será consumido.

import Http from './Http'

/**
 * @type Api
 */
export default class Api extends Http {
  /**
   * @type {String}
   */
  static base = '/api/v1'

  /**
   * @param {String} path
   * @param {Object} options
   * @param {Object} http
   * @return {this}
   */
  static build (path = '', options = {}, http = null) {
    return new this(Api.normalize(Api.base, path), options, http)
  }
}

Com a abstração da API pronta podemos construir uma abstração para o Rest parecida com essa abaixo.

import Api from './Api'

/**
 * @typedef {Rest}
 */
export default class Rest extends Api {
  /**
   * @type {String}
   */
  static resource = ''

  /**
   * @type {String}
   */
  id = 'id'

  /**
   * @param {String} resource
   * @param {Object} options
   * @param {Object} http
   */
  constructor (resource, options = {}, http = null) {
    super(Rest.normalize(Rest.base, resource), options, http)
  }

  /**
   * @return {this}
   */
  static build () {
    return new this(this.resource)
  }

  /**
   * @param {Object} record
   * @returns {*|PromiseLike<T | never>|Promise<T | never>}
   */
  create (record) {
    return this.post('', record)
  }

  /**
   * @param {String|Object} record
   * @returns {*|PromiseLike<T | never>|Promise<T | never>}
   */
  read (record) {
    return this.get(`/${this.getId(record)}`)
  }

  /**
   * @param {Object} record
   * @returns {*|PromiseLike<T | never>|Promise<T | never>}
   */
  update (record) {
    return this.patch(`/${this.getId(record)}`, record)
  }

  /**
   * @param {Object} record
   * @returns {*|PromiseLike<T | never>|Promise<T | never>}
   */
  destroy (record) {
    return this.delete(`/${this.getId(record)}`)
  }

  /**
   * @param {Object} parameters
   * @returns {*|PromiseLike<T | never>|Promise<T | never>}
   */
  search (parameters = {}) {
    const queryString = ''
    // apply your logic here
    return this.get(`?${queryString}`).then(response => ({
      rows: response.rows // just an example
    }))
  }

  /**
   * @param {String|Object} record
   * @returns {String}
   */
  getId (record) {
    if (typeof record === 'object') {
      return record[this.id]
    }
    return String(record)
  }
}

Como eu uso isso tudo agora?

O primeiro passo é criar uma especialização do Rest para o domínio da sua entidade / recurso. Vamos imaginar que você vá consumir a API de carros como na imagem lá de cima.

import Rest from 'src/app/Services/Rest'

/**
 * @typedef {CarService}
 */
export default class CarService extends Rest {
  /**
   * @type {String}
   */
  static resource = '/cars'
}

Com essa abstração criada você pode fazer esse serviço chegar no seu componente e usar os métodos de CRUD (create, read, update, delete) que ele possui para gerenciar sua coleção. Para usar ele no seu componente poderia fazer algo como o exemplo abaixo.

<template>
  <table>
    <tr>
      <th>Name</th>
      <th>Price</th>
    </tr>
    <tr
      v-for="(row, key) in rows"
      :key="row['id']"
    >
      <td>{{ row['name'] }}</td>
      <td>{{ row['price'] | money }}</td>
    </tr>
  </table>
</template>

<script>
import { money } from 'src/support/formatter'
import CarService from 'src/domains/Car/CarService'

const service = CarService.build()

export default {
  name: 'CarTable',
  data: () => ({
    rows: []
  }),
  filters: {
    money: money
  },
  methods: {
    fetchRecords () {
      service
        .search({})
        .then(this.fetchRecodsSuccess)
    },
    fetchRecodsSuccess (response) {
      if (Array.isArray(response.rows)) {
        this.rows = respons.rows
        return
      }
      this.rows = []
    }
  },
  mounted () {
    this.fetchRecords()
  }
}
</script>

Conclusão

Como eu disse no artigo anterior essa abordagem de serviços nos permite alternar facilmente sobre o que está sendo usado para o consumo da API. Com algum pouco esforço seria possível criar uma estrutura semelhante para usar com GraphQL (próximos capítulos) e usar da mesma forma dentro dos seus componentes.

A separação da lógica da sua aplicação em si da lógica operacional da apresentação dos componentes é um marco na sua produção de software, pois você terá vantagens em vários aspectos.