DEV Community

Tobias Mesquita for Quasar Framework Brasil

Posted on

QPANC - Parte 14 - Quasar - Consumindo a API

QPANC são as iniciais de Quasar PostgreSQL ASP NET Core.

30 Axios

Durante a criação do projeto do Quasar, você pode optar por criar ou não o boot do axios, mas mesmo que tenha o criado, ele não está adequado para uma aplicação SSR.

O nosso primeiro passo, será configurar uma rede interna no arquivo de configuração do docker-compose, para que o aplicativo do Quasar possa se comunicar com a API.

QPANC/docker-compose.yml

services:
  qpanc.api:
    networks:
      qpanc.network:
      qpanc.internal:
        ipv4_address: "172.18.18.3"

  qpanc.app:
    networks:
      qpanc.network:
      qpanc.internal:
        ipv4_address: "172.18.18.6"

  qpanc.database:
    networks:
      qpanc.network:
      qpanc.internal:
        ipv4_address: "172.18.18.9"

networks:
  qpanc.internal:
    internal: true
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.18.18.0/24
  qpanc.network:

Durante o desenvolvimento, o comando quasar dev não é executado durante a construção da imagem, ele será executado quando tentamos levantar o container com a imagem de desenvolvimento.

por isto, precisamos setar as variáveis API_CLIENT_URL e API_SERVER_URL direto no comando, junto ao quasar dev. lembrando que esta alteração deve ser feita no docker-compose.override.yml.

QPANC/docker-compose.override.yml

services:
  qpanc.app:
    command: /bin/sh -c "yarn && API_CLIENT_URL=https://localhost:34513/ API_SERVER_URL=https://172.18.18.3:443/ quasar dev -m ssr"

Agora, precisamos modificar o quasar.config.js > build > extendWebpack, para injetamos a URL da API.

QPANC.App/quasar.config.js

const { DefinePlugin } = require('webpack')

module.exports = function (ctx) {
  return {
    build: {
      extendWebpack (cfg, { isServer }) {
        const definePlugin = cfg.plugins.find(plugin => plugin instanceof DefinePlugin)
        const apiUrl = JSON.stringify(isServer ? process.env.API_SERVER_URL : process.env.API_CLIENT_URL)
        definePlugin.definitions['process.env'].API_URL = apiUrl
      }
    }
  }
}

Estamos usando o extendWebpack, pois o API_URL deve ter um valor diferente no build: CLIENT quando comparado com o build: SERVER, quando não é necessário fazer esta diferenciação, podemos usar o quasar.config.js > build > env.

Agora, voltemos a nossa atenção para o boot do axios.

import axios from 'axios'
import inject from './inject'
import { Notify } from 'quasar'

export default inject(({ store, router }) => {
  const instance = axios.create({
    baseURL: process.env.API_URL
  })

  instance.interceptors.request.use((config) => {
    const token = store.state.app.token
    const locale = store.getters['app/locale']
    config.headers['Accept-Language'] = locale
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  }, (error) => {
    return Promise.reject(error)
  })

  instance.interceptors.response.use((response) => {
    return response
  }, (error) => {
    const { status } = error.response || {}
    let message = store.$t('http.generic')
    switch (status) {
      case 400: message = store.$t('http.badRequest'); break
      case 401: message = store.$t('http.unauthorized'); break
      case 403: message = store.$t('http.forbidden'); break
      case 422:
        if (error.response.data) {
          const { title, errors } = error.response.data
          message = title
          message += '<ul>'
          for (const key in errors) {
            const error = errors[key]
            for (const msg of error) {
              message += '<li>' + msg + '</li>'
            }
          }
          message += '</ul>'
          store.$root.$emit('unprocessable', errors)
        } else {
          message = store.$t('http.unprocessable')
        }
        break
      case 500: message = store.$t('http.serverError'); break
      case 503: message = store.$t('http.serviceUnavailable'); break
    }
    Notify.create({
      type: 'negative',
      html: true,
      message: message
    })
    if (status === 401) {
      store.commit('app/token', undefined)
      router.push('/login')
    }
    return Promise.reject(error)
  })

  return {
    axios: instance
  }
})

Um pequeno detalhamento sobre o que está sendo feito acima:

  const instance = axios.create({
    baseURL: process.env.API_URL
  })

Aqui estamos verificando se estamos do lado do servidor ou do cliente, e criando uma instancia do axios com a URL apropriada.

  instance.interceptors.request.use((config) => {
    const token = store.state.app.token
    const locale = store.getters['app/locale']
    config.headers['Accept-Language'] = locale
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  }, (error) => {
    return Promise.reject(error)
  })

Aqui estamos interceptando todas as requisições feitas com o axios, e adicionando o header de autenticação Authorization: Bearer ${token}, assim como a linguagem definida pelo usuário (que pode ser diferente da do browser/OS).

  instance.interceptors.response.use((response) => {
    return response
  }, (error) => {
    const { status } = error.response || {}
    let message = i18n.t('http.generic')
    switch (status) {
      case 400: message = store.$t('http.badRequest'); break
      case 401: message = store.$t('http.unauthorized'); break
      case 403: message = store.$t('http.forbidden'); break
      case 422:
        ...
        break
      case 500: message = store.$t('http.serverError'); break
      case 503: message = store.$t('http.serviceUnavailable'); break
    }
    Notify.create({
      type: 'negative',
      html: true,
      message: message
    })
    if (status === 401) {
      ...
    }
    return Promise.reject(error)
  })

Neste ponto, estamos interceptando as requisições que falharam, e exibindo uma notificação não intrusiva.

      case 422:
        if (error.response.data) {
          const { title, errors } = error.response.data
          message = title
          message += '<ul>'
          for (const key in errors) {
            const error = errors[key]
            for (const msg of error) {
              message += '<li>' + msg + '</li>'
            }
          }
          message += '</ul>'
          store.$root.$emit('unprocessable', errors)
        } else {
          message = store.$t('http.unprocessable')
        }
        break

Aqui, estamos adicionando um tratamento adicional para o erro 422, estaremos personalizando a notificação para exibir os erros retornados pela API, assim como, emitindo um erro com estes eventos. Não estamos usando o $t para localizar a mensagem, por que a API já está retornando o texto localizado.

Este evento será interceptado pelos componentes, para que eles possam atualizar a UI para exibir o erro da maneira correta.

    if (status === 401) {
      store.commit('app/token', undefined)
      router.push('/login')
    }

E caso a aplicação retorne um 401, possivelmente houve a tentativa de acessar um recurso protegido usando um token invalido, note que, uma falha na autorização resultaria em um error 403

E como estamos regionalizando as mensagens de erro, precisamos incluir os devidos textos os arquivos de internacionalização.

Quasar.App/src/i18n/en-us/index.js

export default {
  http: {
    generic: 'Something not right happened',
    badRequest: 'We aren\'t able of to do your request, please review all the fields',
    unauthorized: 'You aren\'t authorized, please login',
    forbidden: 'You aren\'t allowed to do that action',
    unprocessable: '@:http.badRequest',
    serverError: 'An unexpected error occurred at the API',
    serviceUnavailable: 'An error occurred at the API, mostly because a third party service'
  }
}

Quasar.App/src/i18n/pt-br/index.js

export default {
  http: {
    generic: 'Algo de errado aconteceu',
    badRequest: 'Não foi possivel realizar esta ação, por favor revise os campos',
    unauthorized: 'Você não está logado',
    forbidden: 'Você não está autorizado a realizar esta ação',
    unprocessable: '@:http.badRequest',
    serverError: 'Ocorreu um erro inesperado na API',
    serviceUnavailable: 'Ocorreu um erro na API, possivelmente ao tentar acessar um serviço externo'
  }
}

Discussion (0)