Skip to main content
Data providers are the backbone of Refine applications, acting as the data layer that handles all API communication. This guide will show you how to set up, customize, and work with data providers effectively.

Understanding Data Providers

A data provider is an object that implements methods for CRUD operations and data fetching. Refine uses these methods through data hooks to manage all server communication.
import { DataProvider } from "@refinedev/core";

const dataProvider: DataProvider = {
  getList: ({ resource, pagination, sorters, filters, meta }) => Promise,
  create: ({ resource, variables, meta }) => Promise,
  update: ({ resource, id, variables, meta }) => Promise,
  deleteOne: ({ resource, id, variables, meta }) => Promise,
  getOne: ({ resource, id, meta }) => Promise,
  getApiUrl: () => string,
  // Optional methods
  getMany: ({ resource, ids, meta }) => Promise,
  createMany: ({ resource, variables, meta }) => Promise,
  updateMany: ({ resource, ids, variables, meta }) => Promise,
  deleteMany: ({ resource, ids, variables, meta }) => Promise,
  custom: ({ url, method, filters, sorters, payload, query, headers, meta }) => Promise,
};

Quick Start

1
Choose a Data Provider
2
Refine supports many popular backends out of the box:
3
  • REST API: @refinedev/simple-rest
  • GraphQL: @refinedev/graphql
  • Supabase: @refinedev/supabase
  • Strapi: @refinedev/strapi-v4
  • Hasura: @refinedev/hasura
  • Appwrite: @refinedev/appwrite
  • NestJS CRUD: @refinedev/nestjsx-crud
  • 4
    Install the Package
    5
    npm install @refinedev/simple-rest
    
    6
    Configure in Your App
    7
    import { Refine } from "@refinedev/core";
    import dataProvider from "@refinedev/simple-rest";
    
    const App = () => (
      <Refine
        dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
        resources={[
          {
            name: "posts",
            list: "/posts",
            create: "/posts/create",
            edit: "/posts/edit/:id",
            show: "/posts/show/:id",
          },
        ]}
      >
        {/* Your app */}
      </Refine>
    );
    

    Using Data Hooks

    Once configured, use Refine’s data hooks to interact with your API:

    Fetching Lists

    import { useList } from "@refinedev/core";
    
    const PostList = () => {
      const { data, isLoading } = useList({
        resource: "posts",
        pagination: { currentPage: 1, pageSize: 10 },
        sorters: [{ field: "createdAt", order: "desc" }],
        filters: [
          { field: "status", operator: "eq", value: "published" },
        ],
      });
    
      if (isLoading) return <div>Loading...</div>;
    
      return (
        <ul>
          {data?.data.map((post) => (
            <li key={post.id}>{post.title}</li>
          ))}
        </ul>
      );
    };
    

    Creating Records

    import { useCreate } from "@refinedev/core";
    
    const CreatePost = () => {
      const { mutate, isLoading } = useCreate();
    
      const handleSubmit = (values) => {
        mutate({
          resource: "posts",
          values: {
            title: values.title,
            content: values.content,
            status: "draft",
          },
        });
      };
    
      return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
    };
    

    Updating Records

    import { useUpdate } from "@refinedev/core";
    
    const EditPost = ({ id }) => {
      const { mutate } = useUpdate();
    
      const handleUpdate = (values) => {
        mutate({
          resource: "posts",
          id,
          values,
        });
      };
    
      return <form onSubmit={handleUpdate}>{/* form fields */}</form>;
    };
    

    Multiple Data Providers

    You can use multiple data providers in a single application:
    import { Refine } from "@refinedev/core";
    import dataProvider from "@refinedev/simple-rest";
    import { DataProvider } from "@refinedev/strapi-v4";
    
    const App = () => (
      <Refine
        dataProvider={{
          default: dataProvider("https://api.example.com"),
          strapi: DataProvider("https://strapi.example.com/api"),
        }}
        resources={[
          {
            name: "posts",
            // Uses default data provider
          },
          {
            name: "products",
            meta: {
              dataProviderName: "strapi",
            },
          },
        ]}
      />
    );
    
    You can also specify the data provider in hooks:
    useList({
      resource: "products",
      dataProviderName: "strapi",
    });
    

    Customizing Data Providers

    Overriding Methods

    Customize specific methods while keeping the rest:
    import dataProvider from "@refinedev/simple-rest";
    
    const API_URL = "https://api.example.com";
    const baseDataProvider = dataProvider(API_URL);
    
    const customDataProvider = {
      ...baseDataProvider,
      create: async ({ resource, variables, meta }) => {
        // Add custom headers
        const response = await fetch(`${API_URL}/${resource}`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-Custom-Header": "value",
          },
          body: JSON.stringify(variables),
        });
    
        const data = await response.json();
    
        return { data };
      },
    };
    

    Using Meta for Custom Parameters

    Pass custom parameters through the meta field:
    // In your component
    useList({
      resource: "posts",
      meta: {
        headers: {
          "X-Custom-Header": "special-value",
        },
        locale: "en-US",
      },
    });
    
    // In your data provider
    const customDataProvider = {
      getList: async ({ resource, meta }) => {
        const { headers, locale } = meta || {};
        
        const response = await httpClient.get(
          `${apiUrl}/${resource}?locale=${locale}`,
          { headers }
        );
    
        return {
          data: response.data,
          total: response.headers["x-total-count"],
        };
      },
    };
    

    Creating a Custom Data Provider

    1
    Define the Structure
    2
    import { DataProvider } from "@refinedev/core";
    import axios from "axios";
    
    export const customDataProvider = (apiUrl: string): DataProvider => ({
      getApiUrl: () => apiUrl,
      
      // Implement required methods
      getList: async ({ resource, pagination, sorters, filters }) => {
        const url = `${apiUrl}/${resource}`;
        
        // Build query parameters
        const params = {
          page: pagination?.currentPage || 1,
          limit: pagination?.pageSize || 10,
        };
    
        const { data } = await axios.get(url, { params });
    
        return {
          data: data.items,
          total: data.total,
        };
      },
    
      getOne: async ({ resource, id }) => {
        const { data } = await axios.get(`${apiUrl}/${resource}/${id}`);
        return { data };
      },
    
      create: async ({ resource, variables }) => {
        const { data } = await axios.post(`${apiUrl}/${resource}`, variables);
        return { data };
      },
    
      update: async ({ resource, id, variables }) => {
        const { data } = await axios.put(`${apiUrl}/${resource}/${id}`, variables);
        return { data };
      },
    
      deleteOne: async ({ resource, id }) => {
        const { data } = await axios.delete(`${apiUrl}/${resource}/${id}`);
        return { data };
      },
    });
    
    3
    Add Error Handling
    4
    import { HttpError } from "@refinedev/core";
    import axios from "axios";
    
    const axiosInstance = axios.create();
    
    axiosInstance.interceptors.response.use(
      (response) => response,
      (error) => {
        const customError: HttpError = {
          ...error,
          message: error.response?.data?.message || error.message,
          statusCode: error.response?.status || 500,
        };
        return Promise.reject(customError);
      }
    );
    
    5
    Implement Filtering and Sorting
    6
    getList: async ({ resource, pagination, sorters, filters }) => {
      const query: any = {
        page: pagination?.currentPage || 1,
        limit: pagination?.pageSize || 10,
      };
    
      // Add sorting
      if (sorters && sorters.length > 0) {
        query.sort = sorters.map(s => `${s.field}:${s.order}`).join(",");
      }
    
      // Add filtering
      if (filters) {
        filters.forEach((filter) => {
          if (filter.operator === "eq") {
            query[filter.field] = filter.value;
          }
        });
      }
    
      const { data } = await axios.get(`${apiUrl}/${resource}`, {
        params: query,
      });
    
      return {
        data: data.items,
        total: data.total,
      };
    },
    

    Working with GraphQL

    For GraphQL APIs, use the @refinedev/graphql package:
    import { Refine } from "@refinedev/core";
    import { GraphQLClient } from "graphql-request";
    import graphqlDataProvider, { liveProvider } from "@refinedev/graphql";
    
    const client = new GraphQLClient("https://api.example.com/graphql");
    
    const App = () => (
      <Refine
        dataProvider={graphqlDataProvider(client)}
        liveProvider={liveProvider(client)}
      />
    );
    

    Best Practices

    Use Type Safety

    import { DataProvider, BaseRecord } from "@refinedev/core";
    
    interface Post extends BaseRecord {
      title: string;
      content: string;
      status: "published" | "draft";
    }
    
    const { data } = useList<Post>({
      resource: "posts",
    });
    
    // data.data is now typed as Post[]
    

    Handle Authentication

    import axios from "axios";
    
    const httpClient = axios.create();
    
    httpClient.interceptors.request.use((config) => {
      const token = localStorage.getItem("token");
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    });
    
    export const dataProvider = customDataProvider(API_URL, httpClient);
    

    Cache Management

    Refine uses TanStack Query for caching. Configure cache behavior:
    import { QueryClient } from "@tanstack/react-query";
    
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          staleTime: 5 * 60 * 1000, // 5 minutes
          cacheTime: 10 * 60 * 1000, // 10 minutes
          refetchOnWindowFocus: false,
        },
      },
    });
    
    const App = () => (
      <QueryClientProvider client={queryClient}>
        <Refine {...config} />
      </QueryClientProvider>
    );
    

    Common Patterns

    Batch Operations

    Implement bulk actions for better performance:
    deleteMany: async ({ resource, ids }) => {
      // Single request for multiple deletions
      const { data } = await axios.delete(`${apiUrl}/${resource}/bulk`, {
        data: { ids },
      });
      
      return { data };
    },
    

    Request Cancellation

    import { CancelToken } from "axios";
    
    getList: async ({ resource, meta }) => {
      const source = CancelToken.source();
      
      try {
        const { data } = await axios.get(`${apiUrl}/${resource}`, {
          cancelToken: source.token,
        });
        return { data: data.items, total: data.total };
      } catch (error) {
        if (axios.isCancel(error)) {
          console.log("Request canceled");
        }
        throw error;
      }
    },
    

    Optimistic Updates

    const { mutate } = useUpdate();
    
    mutate(
      {
        resource: "posts",
        id: 1,
        values: { title: "Updated" },
      },
      {
        onMutate: async (variables) => {
          // Cancel outgoing refetches
          await queryClient.cancelQueries(["posts", "list"]);
          
          // Optimistically update cache
          const previousData = queryClient.getQueryData(["posts", "list"]);
          queryClient.setQueryData(["posts", "list"], (old) => {
            // Update the cache
          });
          
          return { previousData };
        },
        onError: (err, variables, context) => {
          // Rollback on error
          queryClient.setQueryData(
            ["posts", "list"],
            context?.previousData
          );
        },
      }
    );
    

    Troubleshooting

    CORS Issues

    If you encounter CORS errors:
    1. Configure your backend to allow your frontend origin
    2. Use a proxy in development:
    // vite.config.ts
    export default defineConfig({
      server: {
        proxy: {
          "/api": {
            target: "https://api.example.com",
            changeOrigin: true,
          },
        },
      },
    });
    

    Debugging Requests

    Add logging to your data provider:
    const dataProvider = {
      getList: async (params) => {
        console.log("getList called with:", params);
        const result = await baseDataProvider.getList(params);
        console.log("getList returned:", result);
        return result;
      },
    };
    

    Next Steps

    Build docs developers (and LLMs) love