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: 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.