# Data Providers

Main purpose of Vuetify Admin is to manage remote resources from a given API. As soon as it has to communicate with your backend API for any standardized CRUD operation, it calls the adapted method on your provider that will be responsible for fetching or updating resource data.

// Fetching books
let { data, total } = await provider.getList("books", { page: 1, perPage: 10 });
console.log(data)

// Create new book
await provider.create("books", { data: { title: "My title" } });

// Fetching one book
let { data } = await provider.getOne("books", { id: 1 });
console.log(book)

// Update title book
await provider.update("books", { id: book.id, data: { title: "New title" } });

// Delete book
await provider.delete("books", { id: book.id });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

All fetching methods of a data provider are standardized in order to ensure compatibility between Vuetify Admin with any API server. This is the adapter pattern which allows all kind of different provider for each type of backend of any exchange protocol, whether it be REST, GraphQL, or even SOAP...

Data Provider

In order to give to Vuetify Admin the ability of fetching remote resource data, you must inject a specific data provider into his constructor as explained in next chapiter.

# API Contract

As always for any adapter pattern approach, all data providers must respect a given contract in order to allow communication with Vuetify Admin. Next object represents the minimal contract that must be implemented :

const dataProvider = {
  getList:    (resource, params) => Promise,
  getOne:     (resource, params) => Promise,
  getMany:    (resource, params) => Promise,
  create:     (resource, params) => Promise,
  update:     (resource, params) => Promise,
  updateMany: (resource, params) => Promise,
  delete:     (resource, params) => Promise,
  deleteMany: (resource, params) => Promise,
}
1
2
3
4
5
6
7
8
9
10

HIERARCHICAL DATA SUPPORT

Note as you can add additional specific methods for hierarchical data support. See dedicated section on main treeview component which made use of this methods.

# Supported API operation methods

# Translatable resources

In case of a translatable resource, Vuetify Admin will add an additional locale property into params object. It's up to you to push this locale context to your API server inside your provider. For instance you can just add a new locale parameter in API query string as next : /books/1?locale=fr. Then it's the backend to do the remaining job, i.e. fetching the targeted field locale in case of resource reading, or save the text field on targeted locale in case of resource creating/editing.

# Included data providers

Vuetify Admin includes a few different data providers for some common backend that implements previous contract. Each of those translates the VA JS arguments to API calls for fetching data.

You can use it them a base example for implementing yours. If you use standard REST API protocol, only few lines has to be changed, mainly for GET_LIST part and error handling.

# General data provider usage

The usage of included data providers are very similar. As first constructor argument, they can takes either a simple API URL or a custom HTTP client of your choice. Both axios or included native fetchJson client are compatible. Use full object if you need to share some headers across all admin app, notably useful for any authentication related headers.

# With simple URL

src/plugins/admin.js

import { jsonServerDataProvider } from "vuetify-admin/src/providers";

const baseURL = process.env.VUE_APP_API_URL || "https://jsonplaceholder.okami101.io";

export default new VuetifyAdmin({
  ...
  dataProvider: jsonServerDataProvider(baseURL),
  ...
});
1
2
3
4
5
6
7
8
9

# With fetchJson client

FetchJson takes the API URL followed by some options are relative constructor arguments. The options allows you to share some common generic data across all api calls as authentication headers.

src/plugins/admin.js

import { jsonServerDataProvider, jwtAuthProvider } from "vuetify-admin/src/providers";
import { FetchJson } from "vuetify-admin/src/providers";

const apiURL = process.env.VUE_APP_API_URL || "http://localhost:8080";

/**
 * Create fetch instance with custom authentication headers
 */
const http = new FetchJson(apiURL, {
  headers: () => {
    let headers = new Headers({
      Accept: "application/json",
    });

    let token = localStorage.getItem("jwt_token");

    if (token) {
      headers.set("Authorization", `Bearer ${token}`);
    }
    return headers;
  },
});

export default new VuetifyAdmin({
  ...
  dataProvider: jsonServerDataProvider(http),
  authProvider: jwtAuthProvider(http),
  ...
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

DYNAMIC HEADERS

For authentication, you may use a function on headers for dynamic token fetching.

CUSTOM CLIENT

If needed you can even have your custom HTTP client that follow generic same contracts as the above one. Check the source (opens new window) of fetchJson.

# With axios

src/plugins/admin.js

import { jsonServerDataProvider } from "vuetify-admin/src/providers";
import axios from "axios";

const baseURL = process.env.VUE_APP_API_URL || "http://localhost:8080";

/**
 * Create axios instance which will send cookies for each request
 */
const http = axios.create({
  baseURL,
  withCredentials: true,
  headers: { "X-Requested-With": "XMLHttpRequest" },
});

export default new VuetifyAdmin({
  ...
  dataProvider: jsonServerDataProvider(http),
  ...
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# API calls

Each next section will describe the VA methods to API calls dialects.

# Simple REST

This Data Provider (opens new window) fits REST APIs using simple GET parameters for filters and sorting. This is the dialect used for instance in FakeRest (opens new window).

Method API calls
getList GET /posts?sort=["title","ASC"]&range=[0, 24]&filter={"title":"bar"}
getOne GET /posts/123
getMany GET /posts?filter={"id":[123,456,789]}
create POST /posts
update PUT /posts/123
updateMany Multiple calls to PUT /posts/{id}
delete DELETE /posts/123
deleteMany Multiple calls to DELETE /posts/{id}

TOTAL

The simple REST data provider expects the API to include a Content-Range header in the response to getList as Content-Range: posts 0-10/42.

# JSON Server

This Data Provider (opens new window) fits REST APIs powered by JSON Server (opens new window), such as JSONPlaceholder (opens new window).

Method API calls
getList GET /posts?_sort=title&_order=ASC&_start=0&_end=24&title=bar&_embed=comments&_expand=user
getOne GET /posts/123
getMany GET /posts?id=123&id=456&id=789
create POST /posts/123
update PUT /posts/123
updateMany Multiple calls to PUT /posts/{id}
delete DELETE /posts/123
deleteMany Multiple calls to DELETE /posts/{id}

TOTAL

The JSON Server REST Data Provider expects the API to include a X-Total-Count header in the response to getList as X-Total-Count: 42.

# Hydra REST

This Data Provider (opens new window) fits REST APIs powered by API Platform (opens new window) on Hydra dialect.

Method API calls
getList GET /posts?page=1&itemsPerPage=15&order={"title":"desc"}&filter={"q":"douglas"}
getOne GET /posts/123
getMany Multiple calls to GET /posts/123
create POST /posts/123
update PUT /posts/123
updateMany Multiple calls to PUT PUT /posts/{id}
delete DELETE /posts/123
deleteMany Multiple calls to DELETE DELETE /posts/{id}

TOTAL

The Hydra REST Data Provider expects the API to include a hydra:totalItems included to the Hydra body along to the hydra:member property which contains the data collection.

# Laravel Query Builder

This Data Provider (opens new window) fits REST APIs powered by Laravel combined to Laravel Query Builder (opens new window) for resource browsing.

It perfectly fits the backend provided by the official Laravel Admin (opens new window) composer package as explained on Laravel guide.

Method API calls
getList GET /posts?fields[posts]=id,title,summary&include=media&page=1&perPage=15&sort=title,-date&filter={"q":"douglas"}
getOne GET /posts/123
getMany GET /posts?filter={"id":[123,456,789]}
create POST /posts
update PUT /posts/123
updateMany Multiple calls to PUT /posts/{id}
delete DELETE /posts/1
deleteMany Multiple calls to DELETE /posts/{id}

For DESC sorting, we use a simple dash before the sortable field. Multiple sort is supported by simply adding more sortable fields separated by comma. The include parameter is used for on demand eager loading relation.

TOTAL

The Laravel Data Provider expects the API to include a meta.total property included to the final JSON body along to the data collection.

In order to work, this provider needs an specific axios instance at first constructor argument :

src/plugins/admin.js

import { laravelDataProvider } from "vuetify-admin/src/providers";
import axios from "axios";

const http = axios.create({
  baseURL,
  withCredentials: true,
  headers: { "X-Requested-With": "XMLHttpRequest" },
});

let dataProvider = laravelDataProvider(http)
1
2
3
4
5
6
7
8
9
10

It allows you to have consistent request client across all providers, by taking cookies credentials, set baseURL, adding any custom HTTP headers as JWT token, using axios request interceptors, etc. You can even add specific provider baseURL if needed via second function arguments laravelDataProvider(http, '/api').

FORM DATA

Laravel Data Provider use classic FormData for all api calls instead of simple JSON. It provides better Laravel integration for file uploads with ready-to-go file validation as well as UploadedFile auto conversion object.
For better reusability, a dedicated converter is available here (opens new window). To use it for your own provider, simply import it by import objectToFormData from "vuetify-admin/src/providers/utils/objectToFormData";

# Writing your own data provider

As seen previously, each provider method takes 2 arguments :

  • resource : represents the string name of concerned resource, should be the resource API URL base for each call.
  • params : a given object adapted for each type of API call.

# Method call signatures

Next board represents what object format you should expects as second params function arguments for each provider method.

Method Description Parameters format
getList Search for resources { pagination: { page: Number , perPage: Number }, sort: [{ by: String, desc: Boolean }], filter: Object }, include: String[], fields: { [resource]: String[] } }
getOne Fetch one resource by id { id: Any }
getMany Fetch multiple resource by ids { ids: Array, include: String[], fields: { [resource]: String[] } }
create Create new resource { data: Object }
update Update existing resource { id: Any, data: Object }
updateMany Update multiple resources { ids: Array, data: Object }
delete Delete existing resource { id: Any }
deleteMany Delete multiple resources { ids: Array }

Here is some valid call examples of Vuetify Admin inside each resource store module :

dataProvider.getList("books", {
  pagination: { page: 1, perPage: 15 },
  sort: [{ by: "publication_date", desc: true }, { by: "title", desc: false }],
  filter: { author: "Cassandra" },
  include: ["media", "reviews"],
  fields: { books: ["isbn", "title"], reviews: ["status", "author"] }
});
dataProvider.getOne("books", { id: 1 });
dataProvider.getMany("books", { ids: [1, 2, 3] });
dataProvider.create("books", { data: { title: "Lorem ipsum" } });
dataProvider.update("books", { id: 1, data: { title: "New title" } });
dataProvider.updateMany("books", { ids: [1, 2, 3], data: { commentable: true } });
dataProvider.delete("books", { id: 1 });
dataProvider.deleteMany("books", { ids: [1, 2, 3] });
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Method response formats

Each provider's method must return a Provider on given format.

Operation Response format
getList { data: Resource[], total: Number }
getOne { data: Resource }
getMany { data: Resource[] }
create { data: Resource }
update { data: Resource }
updateMany empty
delete empty
deleteMany empty

PAGING COUNT

As showed here, in order to make data iterator aware of pager count you'll need to return the total of dataset from server-side.

# Errors handling

In case of any server-side error, i.e. with a response status outside of 2xx range, you just have to return a reject promise with a specific Object with at least a descriptive error message as well as the HTTP status code. This status is transmitted to auth provider in order to allows you specific auth action according to a given status code.

For best error message explanation, it's common to take the message inside the body response in order to get the real server exception. In case of empty message or empty response from server, we fallback to the generic statusText response.

try {
  let response = await axios.post(url, data);
} catch ({ response }) {
  let { data, status, statusText } = response;
  return Promise.reject({
    message: statusText,
    status,
    ...(data || {}),
  });
}
1
2
3
4
5
6
7
8
9
10

The expected error object format :

Property Type Description
message string The error message that will be shown at snackbar via VaMessages component.
status number Response status code, not used by VA but returned on checkError auth provider method.
errors object Use it for server-side validation support.

# Store

You can use all data provider methods for each resource on your custom CRUD pages or any custom authenticated page directly from the Vuex store. You have 2 different methods, one by the mapActions Vuex helper and the other by the global $store instance where you can use the dispatch. The next piece of code will show an example of both ways to fetch data from your providers :

<template>
  <v-row>
    <v-col v-for="item in data" :key="item.id">
      {{ item.name }}
    </v-col>
  </v-row>
</template>

<script>
import { mapActions } from "vuex";

export default {
  data() {
    return {
      data: [],
    }
  },
  async mounted() {
    /**
     * Use the global vuex store instance.
     * You need to provide the name of the resource followed by the provider method you want to call.
     * Each provider methods needs a `params` argument which is the same object described above.
     */
    this.data = await this.$store.dispatch("publishers/getList", {
      pagination: {
        page: 1,
        perPage: 5,
      },
    });

    /**
     * Use the registered global method which use global `api` store module.
     * Then you need to provide a object argument of this format : `{ resource, params }`
     */
    this.data = await this.getList({
      resource: "publishers",
      params: {
        pagination: {
          page: 1,
          perPage: 5,
        },
      },
    });
  },
  methods: {
    ...mapActions({
      getList: "api/getList",
    }),
  },
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51