Skip to main content

Data Provider Integration

Having implemented the authentication logic, we will now integrate it with our data provider to protect our resources from unauthenticated users.

We've obtained the token from our API and stored it in the localStorage in the previous steps. Now, we'll be adding the Authorization header to our API requests.

We'll also be learning about the onError method of the auth provider to handle the authentication-related errors that are thrown by the data provider.

Adding the Authorization Header

Our fake REST API has some resources that require authentication to access them. The authentication will be done by sending the token in the Authorization header.

Let's replace our data provider's fetch method with a custom wrapper that adds the Authorization header to the requests. This way, we'll be able to protect our resources from unauthenticated users and handle this step in a single place.

Update your src/providers/data-provider.ts file by adding the following lines:

src/providers/data-provider.ts
import type { DataProvider } from "@refinedev/core";

const API_URL = "https://api.fake-rest.refine.dev";

const fetcher = async (url: string, options?: RequestInit) => {
return fetch(url, {
...options,
headers: {
...options?.headers,
Authorization: localStorage.getItem("my_access_token"),
},
});
};

export const dataProvider: DataProvider = {
getList: async ({ resource, pagination, filters, sorters, meta }) => {
const params = new URLSearchParams();

if (pagination) { /* ... */ }

if (sorters && sorters.length > 0) { /* ... */ }

if (filters && filters.length > 0) { /* ... */ }

const response = await fetch(
const response = await fetcher(
`${API_URL}/${resource}?${params.toString()}`,
);

if (response.status < 200 || response.status > 299) throw response;

const data = await response.json();

const total = Number(response.headers.get("x-total-count"));

return {
data,
total,
};
},
getMany: async ({ resource, ids, meta }) => {
const params = new URLSearchParams();

if (ids) { /* ... */ }

const response = await fetch(
const response = await fetcher(
`${API_URL}/${resource}?${params.toString()}`,
);

if (response.status < 200 || response.status > 299) throw response;

const data = await response.json();

return { data };
},
getOne: async ({ resource, id, meta }) => {
const response = await fetch(`${API_URL}/${resource}/${id}`);
const response = await fetcher(`${API_URL}/${resource}/${id}`);

if (response.status < 200 || response.status > 299) throw response;

const data = await response.json();

return { data };
},
create: async ({ resource, variables }) => {
const response = await fetch(`${API_URL}/${resource}`, {
const response = await fetcher(`${API_URL}/${resource}`, {
method: "POST",
body: JSON.stringify(variables),
headers: {
"Content-Type": "application/json",
},
});

if (response.status < 200 || response.status > 299) throw response;

const data = await response.json();

return { data };
},
update: async ({ resource, id, variables }) => {
const response = await fetch(`${API_URL}/${resource}/${id}`, {
const response = await fetcher(`${API_URL}/${resource}/${id}`, {
method: "PATCH",
body: JSON.stringify(variables),
headers: {
"Content-Type": "application/json",
},
});

if (response.status < 200 || response.status > 299) throw response;

const data = await response.json();

return { data };
},
/* ... */
};

Using a Restricted Resource

Our fake REST API also has a resource called protected-products that is same as the products resource, but requires authentication to access it for demonstration purposes.

Let's switch to using protected-resources in our <ListProducts /> component.

Update your src/pages/products/list.tsx file by adding the following lines:

src/pages/products/list.tsx
import { useTable, useMany } from "@refinedev/core";

export const ListProducts = () => {
const {
tableQuery: { data, isLoading },
current,
setCurrent,
pageCount,
sorters,
setSorters,
} = useTable({
resource: "protected-products",
pagination: { current: 1, pageSize: 10 },
sorters: { initial: [{ field: "id", order: "asc" }] },
});

const { data: categories } = useMany({
resource: "categories",
ids: data?.data?.map((product) => product.category?.id) ?? [],
});

if (isLoading) {
return <div>Loading...</div>;
}

const onPrevious = () => {
/* ... */
};
const onNext = () => {
/* ... */
};
const onPage = (page: number) => {
/* ... */
};
const getSorter = (field: string) => {
/* ... */
};
const onSort = (field: string) => {
/* ... */
};

const indicator = { asc: "⬆️", desc: "⬇️" };

return <div>{/* ... */}</div>;
};

Handling Authentication Errors

When we try to access a protected resource without authentication, our API will return a 401 Unauthorized error. We'll be handling this error in our auth provider's onError method.

onError will be called by the Refine's data hooks automatically when an error is thrown from the data provider. This method does not need to handle all errors, only the ones related to authentication.

By using onError method, we'll be able to handle cases such as expired and invalid tokens and trigger a logout operation.

Let's add the onError method to our auth provider and handle the 401 Unauthorized error.

Update your src/providers/auth-provider.ts file by adding the following lines:

src/providers/auth-provider.ts
import { AuthProvider } from "@refinedev/core";

export const authProvider: AuthProvider = {
onError: async (error) => {
if (error?.status === 401) {
return {
logout: true,
error: { message: "Unauthorized" },
};
}

return {};
},
getIdentity: async () => {
/* ... */
},
logout: async () => {
/* ... */
},
login: async ({ email, password }) => {
/* ... */
},
check: async () => {
/* ... */
},
// ...
};

Finally, we'll be able to handle the 401 Unauthorized error thrown from the data providers and trigger a logout operation.

Summary

Now we have our authentication mechanism integrated with Refine, additional methods can be implemented in the same way and used with the respective hooks of Refine.

All of the built-in data providers of Refine have the ability to customize the client/fetcher instance. They can be used to handle authentication in the same way as we did in this tutorial without requiring a custom data provider.

In the next units, we'll start learning about the routing in Refine and how to integrate routing solutions such as React Router.

Current way of handling authentication can be refactored but the concepts will remain the same.

Was this helpful?
import React from "react";
import { useLogout, useGetIdentity } from "@refinedev/core";

export const Header = () => {
  const { mutate, isLoading } = useLogout();
  const { data: identity } = useGetIdentity();

  return (
    <>
      <h2>
        <span>Welcome, </span>
        <span>{identity?.name ?? ""}</span>
      </h2>
      <button type="button" disabled={isLoading} onClick={mutate}>
        Logout
      </button>
    </>
  );
};
installing dependencies
installing dependencies