I'm new to Vue.js Nuxt and all front-end stuff.
I have a question about API calls. I'm not sure what is the right way, the best practice here.
I have a store. In that store, I have actions that are calling my API and sets state eg.
async fetchArticle({ state, commit }, uuid) {
const response = await this.$axios.get(`articles/${uuid}/`)
commit('SET_ARTICLE', response.data)
},
And that is fine it is working for one component.
But what if I want to just fetch the article and not changing the state.
To be DRY first thing that comes to my mind is to create the service layer that is fetching the data and is used where it is needed.
Is it the right approach? Where can I find some real-world examples that I can take inspiration from?
Using the repository pattern to abstract your API is definitely a good idea! Whether you use the #nuxtjs/axios module or the #nuxt/http module, you can pass either instance to your repository class/function. Below a real world example of an abstracted "repository.js" file.
export default $axios => resource => ({
index() {
return $axios.$get(`/${resource}`)
},
create(payload) {
return $axios.$post(`/${resource}`, payload)
},
show(id) {
return $axios.$get(`/${resource}/${id}`)
},
update(payload, id) {
return $axios.$put(`/${resource}/${id}`, payload)
},
delete(id) {
return $axios.$delete(`/${resource}/${id}`)
}
})
You can then create a plugin to initialize all different kinds of repositories for your endpoints:
import createRepository from '~/path/to/repository.js'
export default (ctx, inject) => {
const repositoryWithAxios = createRepository(ctx.$axios)
const repositories = {
posts: repositoryWithAxios('posts'),
users: repositoryWithAxios('users')
//...
}
inject('repositories', repositories)
}
Further read: Organize and decouple your API calls in Nuxt.js
I will an example of a service layer implementation for my portfolio to create my dashboard that shows some statics about my github and stackoverflow profiles, to do this i created a folder called services inside the project root :
pages
services
|_AxiosConfig.js
|_GitHubService.js
|_StackoverflowService.js
...
in the AxiosConfig.js file i put i created an axios instance with its configuration :
import axios from 'axios';
const clientAPI = url =>
axios.create({
baseURL: url,
withCredentials: false,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
export default clientAPI;
then in my GitHubService.js i imported that axios instance called clientAPI which i used to my requests :
import clientAPI from './AxiosConfig';
const baseURL = 'https://api.github.com';
export default {
getUser(name) {
return clientAPI(baseURL).get('/users/' + name);
},
getRepos(name){
return clientAPI(baseURL).get('/users/' + name+'/repos');
},
getEvents(name,page){
return clientAPI(baseURL).get('/users/' + name+'/events?per_page=100&page='+page);
},
getLastYearCommits(name,repo){
return clientAPI(baseURL).get('/repos/' + name+'/'+repo+'/stats/commit_activity');
}
};
then in my page i used asyncData hook to fetch my data :
import GitHubService from '../../services/GitHubService'
export default {
...
async asyncData({ error }) {
try {
const { data } = await GitHubService.getUser("boussadjra");
const resRepos = await GitHubService.getRepos("boussadjra");
return {
user: data,
repos: resRepos.data
};
} catch (e) {
error({
statusCode: 503,
message: "We cannot find the user"
});
}
}
I wanted to use axios in my service/service.js file, so instead of passing axios, I accessed it directly like this:
export default {
async fetchArticle() {
let response = await $nuxt.$axios.$get('/api-url')
return response
},
}
In Nuxt, if you want to just get the data without keeping it in your store, you could use the asyncData function, which asynchronously loads data (from API calls and the like) and pushes it into the component's data object before rendering.
Related
Is it possible to use global onRequest handler to $fetch with Nuxt3, to add specific data on each request?
With nuxt2 and axios it was simple
/plugins/axios.js
export default function ({ $axios, store, req }) {
$axios.onRequest((config) => {
if (config.data) {
config.data.test = '123';
} else {
config.data = { test: '123' };
}
return config;
});
}
But how achieve same goal on Nuxt3 and $fetch?
Ok, so Nuxt3 $fetch documentation says:
Nuxt uses ofetch to expose globally the $fetch helper...
When we jump into ofetch documentation we can see the Interceptors section. This gives us some options to do what you are trying to achieve. My suggestion is this:
Create a http composable (or anyother name you wish):
// composables/use-http.js
const opts = {
async onRequest({ request, options }) {
// Add your specific data here
options.query = { t: '1234' }
options.headers = { 'Authorization': 'my_token' }
}
}
export default () => $fetch.create(opts)
And here we are making usage of the onRequest interceptor from ofetch
onRequest is called as soon as ofetch is being called, allowing to modify options or just do simple logging.
There you can add any data you want, if you need you can create the logic to pass parameters to this composable and so on...
Now, to actually fetch the data (use the composable):
const http = useHttp() // useHttp is auto-imported
const data = await http('/url') // will trigger the interceptor
In my Nuxt project I have a file named "apiAccess.js" in the root folder. This file simply exports a bunch of functions that make Ajax calls to the server API. This file is imported in any page that needs access to the server API. I need to send a JWT token with each of these api requests, and I have stored that token in the Vuex store.
I need to access the JWT token from the Vuex store within this "apiAccess.js" file. Unfortuntaely, this.$store is not recognized within this file. How do I access the Vuex store from within this file? Or should I have done something differently?
Here's a snippet from the apiAccessjs file where I try to access the store:
import axios from 'axios'
const client = axios.create({
baseURL: 'http://localhost:3000/api',
json: true,
headers: { Authorization: 'Bearer' + this.$store.state.auth.token }
})
After i readed this post i used this generic structure:
// generic actions file
import {
SET_DATA_CONTEXT,
SET_ITEM_CONTEXT
} from '#/types/mutations'
// PAGEACTIONS
export const getDataContext = api => async function ({ commit }) {
const data = await this[api].get()
commit(SET_DATA_CONTEXT, data)
}
export const getItemContext = api => async function ({ commit }, id) {
const data = await this[api].getById(id)
commit(SET_ITEM_CONTEXT, data)
}
export const createItemContext = api => async function ({}, form) {
await this[api].create(form)
}
export const updateItemContext = api => async function ({}, form) {
await this[api].update(form)
}
export const deleteItemContext = api => async function ({}, id) {
await this[api].delete(id)
}
and for any store i used actions from my generic file:
// any store file
import {
getDataContext,
getItemContext,
createItemContext,
updateItemContext,
deleteItemContext,
setDynamicModal
} from '#/use/store.actions'
const API = '$rasterLayerAPI'
export const state = () => ({
dataContext: [],
itemContext: {},
})
export const actions = {
createItemContext: createItemContext(API),
getDataContext: getDataContext(API),
getItemContext: getItemContext(API),
updateItemContext: updateItemContext(API),
deleteItemContext: deleteItemContext(API),
}
because I had many stores with similar features.
and the same for mutations i used generic mutations functions.
Need some help in solving a reload problem.
I fetch data via service:
import axios from 'axios'
const apiClient = axios.create({
baseURL: 'www.domain/api/v1',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
})
and
export default {
getCompanies() {
return apiClient.get('/companies')
},
in store:
export const actions = {
fetchCompanies({ commit }) {
return CompanyService.getCompanies().then(response => {
commit('SET_COMPANIES', response.data)
})
},
in pages/companies:
async fetch({ store, error }) {
try {
await store.dispatch('company/fetchCompanies')
} catch (e) {
error({
statusCode: 503,
message: 'Unable to fetch Companies at this time'
})
}
},
works fine, but no data on page reload.
Some help would be great.
If you are using it on a pagination component the reason it is not reloading is that fetch it will be called server-side once and in the client-side just when navigating to further routes.
The fetch method is not called on query string changes by default. If you want to change this behavior, for example when building a pagination component, you can setup parameters that should be listened to through the watchQuery property of your page component. Learn more on the API watchQuery page.
Looks Nuxt documentation: https://nuxtjs.org/api/pages-fetch/
You can use watchQuery to fix this.
https://nuxtjs.org/api/pages-watchquery
In this article, it says:
While it’s generally poor practice, you can use Axios directly in your components to fetch data from a method, lifecycle hook, or whenever.
I am wondering why? I usually use lifecycle hooks a lot to fetch data (especially from created()). Where should we write the request calls?
Writing API methods directly in components increases code lines and make difficult to read.
As far as I believe the author is suggesting to separate API methods into a Service.
Let's take a case where you have to fetch top posts and operate on data. If you do that in component it is not re-usable, you have to duplicate it in other components where ever you want to use it.
export default {
data: () => ({
top: [],
errors: []
}),
// Fetches posts when the component is created.
created() {
axios.get(`http://jsonplaceholder.typicode.com/posts/top`)
.then(response => {
// flattening the response
this.top = response.data.map(item => {
title: item.title,
timestamp: item.timestamp,
author: item.author
})
})
.catch(e => {
this.errors.push(e)
})
}
}
So when you need to fetch top post in another component you have to duplicate the code.
Now let's put API methods in a Service.
api.js file
const fetchTopPosts = function() {
return axios.get(`http://jsonplaceholder.typicode.com/posts/top`)
.then(response => {
// flattening the response
this.top = response.data.map(item => {
title: item.title,
timestamp: item.timestamp,
author: item.author
})
}) // you can also make a chain.
}
export default {
fetchTopPosts: fetchTopPosts
}
So you use the above API methods in any components you wish.
After this:
import API from 'path_to_api.js_file'
export default {
data: () => ({
top: [],
errors: []
}),
// Fetches posts when the component is created.
created() {
API.fetchTopPosts().then(top => {
this.top = top
})
.catch(e => {
this.errors.push(e)
})
}
}
It's fine for small apps or widgets, but in a real SPA, it's better to abstract away your API into its own module, and if you use vuex, to use actions to call that api module.
Your component should not be concerned with how and from where its data is coming. The component is responsible for UI, not AJAX.
import api from './api.js'
created() {
api.getUsers().then( users => {
this.users = users
})
}
// vs.
created() {
axios.get('/users').then({ data }=> {
this.users = data
})
}
In the above example, your "axios-free" code is not really much shorter, but imagine what you could potentially keep out of the component:
handling HTTP errors, e.g. retrying
pre-formatting data from the server so it fits your component
header configuration (content-type, access token ...)
creating FormData for POSTing e.g. image files
the list can get long. all of that doesn't belong into the component because it has nothing to do with the view. The view only needs the resulting data or error message.
It also means that you can test your components and api independently.
I'm currently working on a new Vue.js application. It depends heavily on api calls to my backend database.
For a lot of things I use Vuex stores because it manages shared data between my components. When looking at other Vue projects on github I see a special vuex directory with files that handles all the actions, states and so on. So when a component has to call the API, it includes the actions file from the vuex directory.
But, for messages for example, I don't want to use Vuex because those data is only important for one specific view. I want to use the component specific data here. But here is my problem: I still need to query my api. But I shouldn't include the Vuex actions file. So in that way I should create a new actions file. This way I have a specific file with api actions for vuex and for single components.
How should I structure this? Creating a new directory 'api' that handles actions for both vuex data and component-specific data? Or separate it?
I am using axios as HTTP client for making api calls, I have created a gateways folder in my src folder and I have put files for each backend, creating axios instances, like following
myApi.js
import axios from 'axios'
export default axios.create({
baseURL: 'http://localhost:3000/api/v1',
timeout: 5000,
headers: {
'X-Auth-Token': 'f2b6637ddf355a476918940289c0be016a4fe99e3b69c83d',
'Content-Type': 'application/json'
}
})
Now in your component, You can have a function which will fetch data from the api like following:
methods: {
getProducts () {
myApi.get('products?id=' + prodId).then(response => this.product = response.data)
}
}
Similarly you can use this to get data for your vuex store as well.
Edited
If you are maintaining product related data in a dedicate vuex module,
you can dispatch an action from the method in component, which will internally call the backend API and populate data in the store, code will look something like following:
Code in component:
methods: {
getProducts (prodId) {
this.$store.dispatch('FETCH_PRODUCTS', prodId)
}
}
Code in vuex store:
import myApi from '../../gateways/my-api'
const state = {
products: []
}
const actions = {
FETCH_PRODUCTS: (state, prodId) => {
myApi.get('products?id=' + prodId).then(response => state.commit('SET_PRODUCTS', response))
}
}
// mutations
const mutations = {
SET_PRODUCTS: (state, data) => {
state.products = Object.assign({}, response.data)
}
}
const getters = {
}
export default {
state,
mutations,
actions,
getters
}
Note: vue-resource is retired ! Use something else, such as Axios.
I'm using mostly Vue Resource.I create services directory, and there put all connections to endpoints, for e.g PostService.js
import Vue from 'vue'
export default {
get(id) {
return Vue.http.get(`/api/post/${id}`)
},
create() {
return Vue.http.post('/api/posts')
}
// etc
}
Then in my file I'm importing that service and create method that would call method from service file
SomeView.vue
import PostService from '../services/PostService'
export default {
data() {
item: []
},
created() {
this.fetchItem()
},
methods: {
fetchItem() {
return PostService.get(to.params.id)
.then(result => {
this.item = result.json()
})
}
}
}
Based on concept of Belmin Bedak`s answer, i have wrapped it all into a simple library:
https://github.com/robsontenorio/vue-api-query
You can request your API like this:
All results
// GET /posts?filter[status]=ACTIVE
let post = await Post
.where('status', 'ACTIVE')
.get()
Specific result
// GET /posts/1
let post = await Post.find(1)
Editing
// PUT /posts/1
post.title = 'Awsome!'
post.save()
Relationships
// GET /users/1
let user = await User.find(1)
// GET users/1/posts
let posts = await user
.posts()
.get()