3. Create Data Provider From Scratch
Introduction
The standards and usage of APIs can vary significantly, so it is possible that refine's built-in data providers may not be suitable for your needs. In this case, you will need to develop your own Data Provider.
Data providers works like an adapter system infrastructure. So they can communicate with REST, GraphQL, RPC and SOAP based APIs. You can use fetch
, axios
, apollo-client
or any library for this communication.
As shown below, we will begin by creating a file and adding additional methods as we proceed.
Using axios
as our HTTP client will allow us to make efficient and reliable HTTP requests to our server. Interceptors provide several benefits, such as centralized error handling, the ability to modify request or response data, and show global loading indicators.
To get started, install the axios
to our project.
- npm
- pnpm
- yarn
npm i axios
pnpm add axios
yarn add axios
Using the stringify
library will allow us to convert the query parameters into a string format. This can be useful when we need to pass query parameters as part of an HTTP request.
- npm
- pnpm
- yarn
npm i query-string@7
pnpm add query-string@7
yarn add query-string@7
For our own data provider, the first step is to create the following file.
import { DataProvider } from "@pankod/refine-core";
import { stringify } from "query-string";
export const dataProvider = (apiUrl: string): DataProvider => ({
// Methods
});
Error Handling
When an error is returned from the API, refine must be extended from HttpError to handle it. Axios interceptor can be used to transform the error from response before Axios returns the response to your code. Interceptors are methods which are triggered before the main method.
In a utility
file, create an axiosInstance
and define an interceptor
to handle errors. Then export it.
import axios from "axios";
import { DataProvider, HttpError } from "@pankod/refine-core";
import { stringify } from "query-string";
// Error handling with axios interceptors
const axiosInstance = axios.create();
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
const customError: HttpError = {
...error,
message: error.response?.data?.message,
statusCode: error.response?.status,
};
return Promise.reject(customError);
},
);
export const dataProvider = (apiUrl: string): DataProvider => ({
// Methods
});
Methods
Now we'll add the methods that the data provider needs to implement. We will implement the following methods:
getList
getList
method is used to get a list of resources with sorting, filtering and pagination features.
It takes resource
, sort
, pagination
and filters
as parameters and returns data
and total
.
Let's assume the API we want to implement is as follows:
[GET] https://api.fake-rest.refine.dev/posts
HTTP/2 200
Content-Type: application/json
x-total-count: 1000
access-control-expose-headers: X-Total-Count
[
{
"id": 930,
"title": "Rerum id laborum architecto et rerum earum.",
"slug": "et-voluptas-corporis",
"category": {
"id": 4
}
"status": "draft",
},
{
"id": 892,
"title": "Architecto officiis sint voluptatem modi.",
"slug": "iusto-est-corrupti",
"category": {
"id": 1
},
"status": "rejected",
}
...
]
In the first step, we'll return the data and the total number of records using the
resource
parameter.resource
parameter is the name of the resource that we want to get the data from. It passes by theresource
parameter in hooks. In our case, it isposts
.src/data-provider.tsexport const dataProvider = (apiUrl: string): DataProvider => ({
// ...
getList: async ({ resource }) => {
const url = `${apiUrl}/${resource}`;
const { data, headers } = await axiosInstance.get(url);
const total = +headers["x-total-count"];
return {
data,
total,
};
},
// ...
});Now let's add the pagination feature. For this, the API takes the following parameters.
[GET] https://api.fake-rest.refine.dev/posts?_limit=10&_page=2
refine uses the
pagination
parameter for pagination. For this parameter,current
is refer to page number andpageSize
refer to the number of records in the each page.[
{
current: 1,
pageSize: 10,
},
]src/data-provider.tsexport const dataProvider = (apiUrl: string): DataProvider => ({
// ...
getList: async ({ resource, pagination }) => {
const url = `${apiUrl}/${resource}`;
const { current = 1, pageSize = 10 } = pagination ?? {};
const query: {
_start?: number;
_end?: number;
} = {
_start: (current - 1) * pageSize,
_end: current * pageSize,
};
const { data, headers } = await axiosInstance.get(
`${url}?${stringify(query)}`,
);
const total = +headers["x-total-count"];
return {
data,
total,
};
},
// ...
});Now let's add the sorting feature. The API expects the following parameters for sorting.
[GET] https://api.fake-rest.refine.dev/posts?_limit=10&_page=2&_sort=id&_order=desc
refine uses the
sort
parameter for sorting. This parameter includesfield
andorder
values. Supports multiple field sorting. CrudSort[] type, it comes in the data provider as follows.[
{
field: "id",
order: "desc",
},
]TIPrefine supports multi-field sorting.
src/data-provider.tsgetList: async ({ resource, pagination, sort }) => {
const url = `${apiUrl}/${resource}`;
const { current = 1, pageSize = 10 } = pagination ?? {};
const query: {
_start?: number;
_end?: number;
_sort?: string;
_order?: string;
} = {
_start: (current - 1) * pageSize,
_end: current * pageSize,
};
if (sort && sort.length > 0) {
query._sort = sort[0].field;
query._order = sort[0].order;
}
const { data, headers } = await axiosInstance.get(
`${url}?${stringify(query)}`,
);
const total = +headers["x-total-count"];
return {
data,
total,
};
},Now let's add the filtering feature. The API expects the following parameters for filtering.
[GET] https://api.fake-rest.refine.dev/posts?_limit=10&_page=2&_sort=id&_order=desc&title_likerefine uses the
filters
parameter for filtering. This parameter containsfield
,operator
andvalue
with type CrudFilters [].[
{
field: "status"
operator: "eq"
value: "published"
},
{
field: "title"
operator: "contain"
value: "Hello"
},
]The
operator
data comes with the CrudOperators type and needs to be mapped to the API. For this, the followingmapOperator
function is written.// Map refine operators to API operators
const mapOperator = (operator: CrudOperators): string => {
switch (operator) {
case "ne":
case "gte":
case "lte":
return `_${operator}`;
case "contains":
return "_like";
case "eq":
default:
return "";
}
};src/data-provider.tsconst generateFilters = (filters?: CrudFilters) => {
const queryFilters: { [key: string]: string } = {};
filters?.map((filter): void => {
if ("field" in filter) {
const { field, operator, value } = filter;
const mappedOperator = mapOperator(operator);
queryFilters[`${field}${mappedOperator}`] = value;
}
});
return queryFilters;
};
getList: async ({ resource, pagination, sort, filters }) => {
const url = `${apiUrl}/${resource}`;
const { current = 1, pageSize = 10 } = pagination ?? {};
const query: {
_start?: number;
_end?: number;
_sort?: string;
_order?: string;
} = {
_start: (current - 1) * pageSize,
_end: current * pageSize,
};
if (sort && sort.length > 0) {
query._sort = sort[0].field;
query._order = sort[0].order;
}
const queryFilters = generateFilters(filters);
const { data, headers } = await axiosInstance.get(
`${url}?${stringify(query)}&${stringify(queryFilters)}`
);
const total = +headers["x-total-count"];
return {
data,
total,
};
},INFORMATIONAlso, conditional filters can be made using
and
andor
. For example:[
{
operator: "or",
value: [
{
operator: "and"
value: [
{
field: "title"
operator: "contain"
value: "Hello"
},
{
field: "age"
operator: "gte"
value: "18"
},
]
},
{
operator: "and"
value: [
{
field: "title"
operator: "contain"
value: "Hello"
},
{
field: "age"
operator: "lte"
value: "18"
},
]
}
]
}
]Parameter Types:
Name Type resource string
hasPagination? boolean
(defaults totrue
)pagination? Pagination
sort? CrudSorting
filters? CrudFilters
refine will consume this getList
method using the useList
or useInfiniteList
data hook.
import { useList } from "@pankod/refine-core";
const { data } = useList({
resource: "posts",
config: {
sort: [
{
field: "id",
order: "desc",
},
],
filters: [
{
field: "title",
operator: "contains",
value: "hello",
},
],
},
});
create
The create
method creates a new record with the resource
and variables
parameters.
export const dataProvider = (apiUrl: string): DataProvider => ({
// ...
create: async ({ resource, variables }) => {
const url = `${apiUrl}/${resource}`;
const { data } = await axiosInstance.post(url, variables);
return {
data,
};
},
// ...
});
Parameter Types
Name | Type | Default |
---|---|---|
resource | string | |
variables | TVariables | {} |
TVariables
is a user defined type which can be passed touseCreate
to typevariables
refine will consume this create
method using the useCreate
data hook.
import { useCreate } from "@pankod/refine-core";
const { mutate } = useCreate();
mutate({
resource: "posts",
values: {
title: "New Post",
},
});
Refer to the useCreate documentation for more information. →
update
The update
method updates the record with the resource
, id
and variables
parameters.
export const dataProvider = (apiUrl: string): DataProvider => ({
// ...
update: async ({ resource, id, variables }) => {
const url = `${apiUrl}/${resource}/${id}`;
const { data } = await axiosInstance.patch(url, variables);
return {
data,
};
},
// ...
});
Parameter Types:
Name | Type | Default |
---|---|---|
resource | string | |
id | BaseKey | |
variables | TVariables | {} |
TVariables
is a user defined type which can be passed touseUpdate
to typevariables
refine will consume this update
method using the useUpdate
data hook.
import { useUpdate } from "@pankod/refine-core";
const { mutate } = useUpdate();
mutate({
resource: "posts",
id: 2,
values: { title: "New Post Title" },
});
Refer to the useUpdate documentation for more information. →
deleteOne
The deleteOne
method delete the record with the resource
and id
parameters.
export const dataProvider = (apiUrl: string): DataProvider => ({
// ...
deleteOne: async ({ resource, id, variables }) => {
const url = `${apiUrl}/${resource}/${id}`;
const { data } = await axiosInstance.delete(url, {
data: variables,
});
return {
data,
};
},
// ...
});
Parameter Types:
Name | Type | Default |
---|---|---|
resource | string | |
id | BaseKey | |
variables | TVariables[] | {} |
TVariables
is a user defined type which can be passed touseDelete
to typevariables
refine will consume this deleteOne
method using the useDelete
data hook.
import { useDelete } from "@pankod/refine-core";
const { mutate } = useDelete();
mutate({ resource: "posts", id: 2 });
Refer to the useDelete documentation for more information. →
getOne
The getOne
method gets the record with the resource
and id
parameters.
export const dataProvider = (apiUrl: string): DataProvider => ({
// ...
getOne: async ({ resource, id }) => {
const url = `${apiUrl}/${resource}/${id}`;
const { data } = await axiosInstance.get(url);
return {
data,
};
},
// ...
});
Parameter Types:
Name | Type | Default |
---|---|---|
resource | string | |
id | BaseKey |
refine will consume this getOne
method using the useOne
data hook.
import { useOne } from "@pankod/refine-core";
const { data } = useOne({ resource: "posts", id: 1 });
getApiUrl
The getApiUrl
method returns the apiUrl
value.
import { DataProvider } from "@pankod/refine-core";
export const dataProvider = (apiUrl: string): DataProvider => ({
getApiUrl: () => apiUrl,
// ...
});
refine will consume this getApiUrl
method using the useApiUrl
data hook.
import { useApiUrl } from "@pankod/refine-core";
const { data } = useApiUrl();
Refer to the useApiUrl documentation for more information. →
custom
An optional method named custom
can be added to handle requests with custom parameters like URL, CRUD methods and configurations.
It's useful if you have non-standard REST API endpoints or want to make a connection with external resources.
export const dataProvider = (apiUrl: string): DataProvider => ({
// ...
custom: async ({ url, method, filters, sort, payload, query, headers }) => {
let requestUrl = `${url}?`;
if (sort && sort.length > 0) {
const sortQuery = {
_sort: sort[0].field,
_order: sort[0].order,
};
requestUrl = `${requestUrl}&${stringify(sortQuery)}`;
}
if (filters) {
const filterQuery = generateFilters(filters);
requestUrl = `${requestUrl}&${stringify(filterQuery)}`;
}
if (query) {
requestUrl = `${requestUrl}&${stringify(query)}`;
}
if (headers) {
axiosInstance.defaults.headers = {
...axiosInstance.defaults.headers,
...headers,
};
}
let axiosResponse;
switch (method) {
case "put":
case "post":
case "patch":
axiosResponse = await axiosInstance[method](url, payload);
break;
case "delete":
axiosResponse = await axiosInstance.delete(url, {
data: payload,
});
break;
default:
axiosResponse = await axiosInstance.get(requestUrl);
break;
}
const { data } = axiosResponse;
return Promise.resolve({ data });
},
// ...
});
Parameter Types
Name | Type |
---|---|
url | string |
method | get , delete , head , options , post , put , patch |
sort? | CrudSorting ; |
filters? | CrudFilters ; |
payload? | {} |
query? | {} |
headers? | {} |
refine will consume this custom
method using the useCustom
data hook.
import { useCustom, useApiUrl } from "@pankod/refine-core";
const { data, isLoading } = useCustom({
url: `${apiURL}/posts-unique-check`,
method: "get",
config: {
query: {
title: "Foo bar",
},
},
});
Refer to the useCustom documentation for more information. →
Bulk Actions
Bulk actions are actions that can be performed on multiple items at once. Performing bulk actions is a common pattern in admin panels. If your API supports bulk actions, you can implement them in your data provider.
getMany
The getMany
method gets the records with the resource
and ids
parameters. Implementation of this method is optional. If you don't implement it, refine will use getOne
method to handle multiple requests.
export const dataProvider = (apiUrl: string): DataProvider => ({
// ...
getMany: async ({ resource, ids }) => {
const { data } = await axiosInstance.get(
`${apiUrl}/${resource}?${stringify({ id: ids })}`,
);
return {
data,
};
},
// ...
});
Parameter Types:
Name | Type | Default |
---|---|---|
resource | string | |
ids | [BaseKey] |
refine will consume this getMany
method using the useMany
data hook.
import { useMany } from "@pankod/refine-core";
const { data } = useMany({ resource: "posts", ids: [1, 2] });
createMany
This method allows us to create multiple items in a resource. Implementation of this method is optional. If you don't implement it, refine will use create
method to handle multiple requests.
export const dataProvider = (apiUrl: string): DataProvider => ({
// ...
createMany: async ({ resource, variables }) => {
const url = `${apiUrl}/${resource}/bulk`;
const { data } = await axiosInstance.post(url, { values: variables });
return {
data,
};
},
// ...
});
Parameter Types:
Name | Type | Default |
---|---|---|
resource | string | |
variables | TVariables[] | {} |
TVariables
is a user defined type which can be passed touseCreateMany
to typevariables
refine will consume this createMany
method using the useCreateMany
data hook.
import { useCreateMany } from "@pankod/refine-core";
const { mutate } = useCreateMany();
mutate({
resource: "posts",
values: [
{
title: "New Post",
},
{
title: "Another New Post",
},
],
});
Refer to the useCreateMany documentation for more information. →
deleteMany
This method allows us to delete multiple items in a resource. Implementation of this method is optional. If you don't implement it, refine will use deleteOne
method to handle multiple requests.
export const dataProvider = (apiUrl: string): DataProvider => ({
// ...
deleteMany: async ({ resource, ids }) => {
const url = `${apiUrl}/${resource}/bulk?ids=${ids.join(",")}`;
const { data } = await axiosInstance.delete(url);
return {
data,
};
},
// ...
});
Parameter Types:
Name | Type | Default |
---|---|---|
resource | string | |
ids | [BaseKey] | |
variables | TVariables[] | {} |
TVariables
is a user defined type which can be passed touseDeleteMany
to typevariables
refine will consume this deleteMany
method using the useDeleteMany
data hook.
import { useDeleteMany } from "@pankod/refine-core";
const { mutate } = useDeleteMany();
mutate({
resource: "posts",
ids: [2, 3],
});
Refer to the useDeleteMany documentation for more information. →
updateMany
This method allows us to update multiple items in a resource. Implementation of this method is optional. If you don't implement it, refine will use update
method to handle multiple requests.
export const dataProvider = (apiUrl: string): DataProvider => ({
// ...
updateMany: async ({ resource, ids, variables }) => {
const url = `${apiUrl}/${resource}/bulk`;
const { data } = await axiosInstance.patch(url, { ids, variables });
return {
data,
};
},
// ...
});
refine will consume this updateMany
method using the useUpdateMany
data hook.
import { useUpdateMany } from "@pankod/refine-core";
const { mutate } = useUpdateMany();
mutate({
resource: "posts",
ids: [1, 2],
values: { status: "draft" },
});
Refer to the useUpdateMany documentation for more information. →
metaData Usage
When using APIs, you may wish to include custom parameters, such as a custom header. To accomplish this, you can utilize the metaData
field, which allows the sent parameter to be easily accessed by the data provider.
Now let's send a custom header parameter to the getOne
method using metaData
.
The metaData
parameter can be used in all data, form, and table hooks.
import { useOne } from "@pankod/refine-core";
useOne({
resource: "post",
id: "1",
metaData: {
headers: {
"x-custom-header": "hello world",
},
},
});
Now let's get the metaData
parameter from the data provider.
import { DataProvider } from "@pankod/refine-core";
export const dataProvider = (apiUrl: string): DataProvider => ({
// ...
getOne: async ({ resource, id, variables, metaData }) => {
const { headers } = metaData;
const url = `${apiUrl}/${resource}/${id}`;
axiosInstance.defaults.headers = {
...headers,
};
const { data } = await axiosInstance.get(url, variables);
return {
data,
};
},
// ...
});