Refine - Framework for CRUD Applications

Learn how to use Refine to build data-intensive applications with built-in authentication, access control, routing, and automatic CRUD operations for admin panels and internal tools.

Refine - Framework for CRUD Applications

Refine

Refine is a React meta-framework for building CRUD-heavy web applications. It provides a complete solution for data-intensive applications like admin panels, dashboards, and internal tools with minimal boilerplate code.

What is Refine?

Refine addresses enterprise use cases by providing industry-standard solutions for:

  • CRUD operations: Automatic create, read, update, delete functionality
  • Authentication: Built-in auth provider system
  • Access control: Role-based permissions
  • Routing: Framework-agnostic routing integration
  • Data provider: Abstract data layer supporting REST, GraphQL, and more
  • State management: Built-in state management
  • i18n: Internationalization support
  • Audit logs: Track all data changes

Key Features

  • Headless architecture: Bring your own UI library or design system
  • Backend agnostic: Works with any REST or GraphQL API
  • Router agnostic: Use React Router, Next.js, or Remix
  • UI framework integrations: Ant Design, Material UI, Mantine, Chakra UI
  • TypeScript first: Full type safety
  • Auto-generated forms: Forms based on your data structure
  • Real-time: Live updates support
  • SSR support: Server-side rendering with Next.js

Why Use Refine?

Traditional Approach

Building a typical admin panel from scratch requires:

// Hundreds of lines for each resource...
// - List page with table, pagination, filters
// - Create form with validation
// - Edit form with data loading
// - Delete confirmation
// - Error handling
// - Loading states
// - Routing
// - Authentication
// - Permissions

With Refine

// Refine handles everything automatically
<Refine
  dataProvider={dataProvider}
  resources={[{
    name: "products",
    list: "/products",
    create: "/products/create",
    edit: "/products/edit/:id",
    show: "/products/show/:id",
  }]}
/>

Installation

Install Refine with your preferred setup:

# Using create-refine-app (recommended)
npm create refine-app@latest my-app

# Manual installation
yarn add @refinedev/core @refinedev/react-router-v6
yarn add react-router-dom

Quick Start with CLI

The CLI guides you through setup:

npm create refine-app@latest my-admin-panel

# Select options:
# - Project name: my-admin-panel
# - Package manager: yarn
# - UI framework: Material UI
# - Backend: REST API
# - Authentication: Custom auth

Basic Setup

Minimal Example

// app/page.tsx
'use client';

import { Refine } from '@refinedev/core';
import { RefineThemes, ThemedLayoutV2 } from '@refinedev/mui';
import dataProvider from '@refinedev/simple-rest';
import routerProvider from '@refinedev/react-router-v6';

export default function App() {
  return (
    <Refine
      dataProvider={dataProvider('https://api.fake-rest.refine.dev')}
      routerProvider={routerProvider}
      resources={[
        {
          name: 'products',
          list: '/products',
          show: '/products/:id',
          create: '/products/create',
          edit: '/products/:id/edit',
        },
      ]}
    >
      <ThemedLayoutV2>
        {/* Your pages will be rendered here */}
      </ThemedLayoutV2>
    </Refine>
  );
}

Data Provider

Data providers abstract your backend API. Refine includes providers for many backends:

Using Simple REST

import dataProvider from '@refinedev/simple-rest';

const API_URL = 'https://api.example.com';

<Refine
  dataProvider={dataProvider(API_URL)}
  // ...
/>

Custom Data Provider

// lib/dataProvider.ts
import { DataProvider } from '@refinedev/core';

export const dataProvider: DataProvider = {
  getList: async ({ resource, pagination, filters, sorters }) => {
    const url = `${API_URL}/${resource}`;
    const { data, headers } = await httpClient.get(url);

    return {
      data,
      total: parseInt(headers['x-total-count']),
    };
  },

  getOne: async ({ resource, id }) => {
    const url = `${API_URL}/${resource}/${id}`;
    const { data } = await httpClient.get(url);

    return { data };
  },

  create: async ({ resource, variables }) => {
    const url = `${API_URL}/${resource}`;
    const { data } = await httpClient.post(url, variables);

    return { data };
  },

  update: async ({ resource, id, variables }) => {
    const url = `${API_URL}/${resource}/${id}`;
    const { data } = await httpClient.patch(url, variables);

    return { data };
  },

  deleteOne: async ({ resource, id }) => {
    const url = `${API_URL}/${resource}/${id}`;
    const { data } = await httpClient.delete(url);

    return { data };
  },

  getApiUrl: () => API_URL,
};

Resources

Resources represent your data entities (users, products, orders, etc.):

<Refine
  dataProvider={dataProvider}
  resources={[
    {
      name: 'products',
      list: '/products',
      create: '/products/create',
      edit: '/products/:id/edit',
      show: '/products/:id',
      meta: {
        icon: <ShoppingCart />,
        canDelete: true,
      },
    },
    {
      name: 'categories',
      list: '/categories',
      create: '/categories/create',
      edit: '/categories/:id/edit',
    },
    {
      name: 'orders',
      list: '/orders',
      show: '/orders/:id',
      meta: {
        icon: <Receipt />,
      },
    },
  ]}
/>

CRUD Operations with Hooks

Refine provides powerful hooks for data operations:

useList - Fetching Lists

'use client';

import { useList } from '@refinedev/core';

interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
}

export function ProductList() {
  const { data, isLoading } = useList<Product>({
    resource: 'products',
    pagination: {
      current: 1,
      pageSize: 10,
    },
    sorters: [
      {
        field: 'createdAt',
        order: 'desc',
      },
    ],
    filters: [
      {
        field: 'category',
        operator: 'eq',
        value: 'electronics',
      },
    ],
  });

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

  return (
    <div>
      {data?.data.map(product => (
        <div key={product.id}>
          <h3>{product.name}</h3>
          <p>${product.price}</p>
        </div>
      ))}
    </div>
  );
}

useOne - Fetching Single Record

import { useOne } from '@refinedev/core';

export function ProductDetail({ id }: { id: string }) {
  const { data, isLoading } = useOne<Product>({
    resource: 'products',
    id,
  });

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

  return (
    <div>
      <h1>{data?.data.name}</h1>
      <p>Price: ${data?.data.price}</p>
      <p>Category: {data?.data.category}</p>
    </div>
  );
}

useCreate - Creating Records

import { useCreate } from '@refinedev/core';

export function CreateProduct() {
  const { mutate, isLoading } = useCreate();

  const handleSubmit = (values: any) => {
    mutate({
      resource: 'products',
      values,
    });
  };

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      const formData = new FormData(e.currentTarget);
      handleSubmit({
        name: formData.get('name'),
        price: Number(formData.get('price')),
      });
    }}>
      <input name="name" placeholder="Product name" required />
      <input name="price" type="number" placeholder="Price" required />
      <button disabled={isLoading}>
        {isLoading ? 'Creating...' : 'Create'}
      </button>
    </form>
  );
}

useUpdate - Updating Records

import { useUpdate } from '@refinedev/core';

export function EditProduct({ id }: { id: string }) {
  const { mutate, isLoading } = useUpdate();

  const handleUpdate = (values: any) => {
    mutate({
      resource: 'products',
      id,
      values,
    });
  };

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      const formData = new FormData(e.currentTarget);
      handleUpdate({
        name: formData.get('name'),
        price: Number(formData.get('price')),
      });
    }}>
      <input name="name" placeholder="Product name" required />
      <input name="price" type="number" placeholder="Price" required />
      <button disabled={isLoading}>
        {isLoading ? 'Updating...' : 'Update'}
      </button>
    </form>
  );
}

useDelete - Deleting Records

import { useDelete } from '@refinedev/core';

export function DeleteProduct({ id }: { id: string }) {
  const { mutate, isLoading } = useDelete();

  const handleDelete = () => {
    if (confirm('Are you sure?')) {
      mutate({
        resource: 'products',
        id,
      });
    }
  };

  return (
    <button onClick={handleDelete} disabled={isLoading}>
      {isLoading ? 'Deleting...' : 'Delete'}
    </button>
  );
}

Authentication

Refine provides a flexible authentication system:

// lib/authProvider.ts
import { AuthProvider } from '@refinedev/core';

export const authProvider: AuthProvider = {
  login: async ({ email, password }) => {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify({ email, password }),
      headers: { 'Content-Type': 'application/json' },
    });

    if (response.ok) {
      const { token } = await response.json();
      localStorage.setItem('auth_token', token);
      return { success: true, redirectTo: '/' };
    }

    return { success: false, error: { message: 'Login failed' } };
  },

  logout: async () => {
    localStorage.removeItem('auth_token');
    return { success: true, redirectTo: '/login' };
  },

  check: async () => {
    const token = localStorage.getItem('auth_token');
    return { authenticated: !!token };
  },

  getIdentity: async () => {
    const token = localStorage.getItem('auth_token');
    if (!token) return null;

    const response = await fetch('/api/auth/me', {
      headers: { Authorization: `Bearer ${token}` },
    });

    return response.json();
  },

  onError: async (error) => {
    if (error.status === 401) {
      return { logout: true, redirectTo: '/login' };
    }
    return {};
  },
};

Using Auth Provider

<Refine
  dataProvider={dataProvider}
  authProvider={authProvider}
  resources={[...]}
/>

Protected Routes

import { Authenticated } from '@refinedev/core';

export function Dashboard() {
  return (
    <Authenticated fallback={<div>Loading...</div>}>
      <div>Protected Dashboard Content</div>
    </Authenticated>
  );
}

UI Framework Integration

With Material UI

import { Refine } from '@refinedev/core';
import {
  ThemedLayoutV2,
  RefineThemes,
  useNotificationProvider,
} from '@refinedev/mui';
import { CssBaseline, ThemeProvider } from '@mui/material';

export default function App() {
  return (
    <ThemeProvider theme={RefineThemes.Blue}>
      <CssBaseline />
      <Refine
        dataProvider={dataProvider}
        notificationProvider={useNotificationProvider}
        resources={[...]}
      >
        <ThemedLayoutV2>
          {/* Your routes */}
        </ThemedLayoutV2>
      </Refine>
    </ThemeProvider>
  );
}

With Ant Design

import { Refine } from '@refinedev/core';
import { ThemedLayoutV2, useNotificationProvider } from '@refinedev/antd';
import '@refinedev/antd/dist/reset.css';

export default function App() {
  return (
    <Refine
      dataProvider={dataProvider}
      notificationProvider={useNotificationProvider}
      resources={[...]}
    >
      <ThemedLayoutV2>
        {/* Your routes */}
      </ThemedLayoutV2>
    </Refine>
  );
}

Tables and Forms

Auto-generated Table

import { useTable } from '@refinedev/react-table';
import { ColumnDef } from '@tanstack/react-table';

export function ProductsTable() {
  const columns: ColumnDef<Product>[] = [
    {
      id: 'name',
      header: 'Name',
      accessorKey: 'name',
    },
    {
      id: 'price',
      header: 'Price',
      accessorKey: 'price',
    },
    {
      id: 'actions',
      header: 'Actions',
      cell: ({ row }) => (
        <>
          <button onClick={() => edit(row.original.id)}>Edit</button>
          <button onClick={() => remove(row.original.id)}>Delete</button>
        </>
      ),
    },
  ];

  const {
    getHeaderGroups,
    getRowModel,
    refineCore: { setCurrent, pageCount, current },
  } = useTable({
    columns,
  });

  return (
    <div>
      <table>
        <thead>
          {getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th key={header.id}>
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {getRowModel().rows.map((row) => (
            <tr key={row.id}>
              {row.getVisibleCells().map((cell) => (
                <td key={cell.id}>
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
      <div>
        <button onClick={() => setCurrent(1)} disabled={current === 1}>
          First
        </button>
        <button onClick={() => setCurrent(current - 1)} disabled={current === 1}>
          Previous
        </button>
        <span>
          Page {current} of {pageCount}
        </span>
        <button
          onClick={() => setCurrent(current + 1)}
          disabled={current === pageCount}
        >
          Next
        </button>
        <button
          onClick={() => setCurrent(pageCount)}
          disabled={current === pageCount}
        >
          Last
        </button>
      </div>
    </div>
  );
}

Auto-generated Forms

import { useForm } from '@refinedev/react-hook-form';

export function ProductForm() {
  const {
    refineCore: { onFinish, formLoading },
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  return (
    <form onSubmit={handleSubmit(onFinish)}>
      <div>
        <label>Name</label>
        <input
          {...register('name', { required: 'Name is required' })}
        />
        {errors.name && <span>{errors.name.message}</span>}
      </div>

      <div>
        <label>Price</label>
        <input
          type="number"
          {...register('price', { required: 'Price is required' })}
        />
        {errors.price && <span>{errors.price.message}</span>}
      </div>

      <button type="submit" disabled={formLoading}>
        {formLoading ? 'Saving...' : 'Save'}
      </button>
    </form>
  );
}

Access Control

Define permissions for resources and actions:

import { AccessControlProvider } from '@refinedev/core';

export const accessControlProvider: AccessControlProvider = {
  can: async ({ resource, action, params }) => {
    const user = await getCurrentUser();

    if (resource === 'products') {
      if (action === 'delete') {
        return { can: user.role === 'admin' };
      }
      if (action === 'create' || action === 'edit') {
        return { can: ['admin', 'editor'].includes(user.role) };
      }
      return { can: true }; // Anyone can list/show
    }

    return { can: true };
  },
};

// Use in Refine
<Refine
  dataProvider={dataProvider}
  accessControlProvider={accessControlProvider}
  resources={[...]}
/>

Conditional Rendering

import { useCan } from '@refinedev/core';

export function ProductActions({ product }: { product: Product }) {
  const { data: canEdit } = useCan({
    resource: 'products',
    action: 'edit',
    params: { id: product.id },
  });

  const { data: canDelete } = useCan({
    resource: 'products',
    action: 'delete',
    params: { id: product.id },
  });

  return (
    <div>
      {canEdit?.can && <button>Edit</button>}
      {canDelete?.can && <button>Delete</button>}
    </div>
  );
}

Real-time Updates

Enable live updates for your data:

import { LiveProvider } from '@refinedev/core';

const liveProvider: LiveProvider = {
  subscribe: ({ channel, types, callback }) => {
    // Subscribe to WebSocket or SSE
    const ws = new WebSocket(`wss://api.example.com/${channel}`);

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      callback(data);
    };

    return ws;
  },

  unsubscribe: (subscription) => {
    subscription.close();
  },

  publish: ({ channel, type, payload }) => {
    // Publish events if needed
  },
};

<Refine
  dataProvider={dataProvider}
  liveProvider={liveProvider}
  options={{ liveMode: 'auto' }}
  resources={[...]}
/>

Complete Example

Here's a complete admin panel example:

'use client';

import { Refine, Authenticated } from '@refinedev/core';
import { ThemedLayoutV2, RefineThemes } from '@refinedev/mui';
import { CssBaseline, ThemeProvider } from '@mui/material';
import dataProvider from '@refinedev/simple-rest';
import routerProvider from '@refinedev/react-router-v6';
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';
import { authProvider } from './authProvider';

import { ProductList, ProductCreate, ProductEdit, ProductShow } from './products';
import { CategoryList, CategoryCreate, CategoryEdit } from './categories';
import { Login } from './auth/login';

export default function App() {
  return (
    <BrowserRouter>
      <ThemeProvider theme={RefineThemes.Blue}>
        <CssBaseline />
        <Refine
          dataProvider={dataProvider('https://api.example.com')}
          routerProvider={routerProvider}
          authProvider={authProvider}
          resources={[
            {
              name: 'products',
              list: '/products',
              create: '/products/create',
              edit: '/products/:id/edit',
              show: '/products/:id',
              meta: {
                canDelete: true,
              },
            },
            {
              name: 'categories',
              list: '/categories',
              create: '/categories/create',
              edit: '/categories/:id/edit',
            },
          ]}
        >
          <Routes>
            <Route
              element={
                <Authenticated fallback={<Login />}>
                  <ThemedLayoutV2>
                    <Outlet />
                  </ThemedLayoutV2>
                </Authenticated>
              }
            >
              <Route path="/products">
                <Route index element={<ProductList />} />
                <Route path="create" element={<ProductCreate />} />
                <Route path=":id" element={<ProductShow />} />
                <Route path=":id/edit" element={<ProductEdit />} />
              </Route>

              <Route path="/categories">
                <Route index element={<CategoryList />} />
                <Route path="create" element={<CategoryCreate />} />
                <Route path=":id/edit" element={<CategoryEdit />} />
              </Route>
            </Route>
          </Routes>
        </Refine>
      </ThemeProvider>
    </BrowserRouter>
  );
}

Best Practices

1. Use TypeScript

interface Product {
  id: string;
  name: string;
  price: number;
  category: Category;
}

const { data } = useList<Product>({
  resource: 'products',
});
// data is typed correctly

2. Organize by Feature

src/
  features/
    products/
      list.tsx
      create.tsx
      edit.tsx
      show.tsx
    categories/
      list.tsx
      create.tsx
  lib/
    dataProvider.ts
    authProvider.ts

3. Use Meta for Extra Information

resources={[
  {
    name: 'products',
    list: '/products',
    meta: {
      label: 'Products',
      icon: <ShoppingCart />,
      canDelete: true,
      priority: 1,
    },
  },
]}

4. Leverage DevTools

import { DevtoolsPanel, DevtoolsProvider } from '@refinedev/devtools';

<DevtoolsProvider>
  <Refine {...}>
    {/* Your app */}
  </Refine>
  <DevtoolsPanel />
</DevtoolsProvider>

Summary

Refine is a powerful framework for building CRUD applications quickly. Key advantages:

  • Rapid development: Build admin panels in hours, not weeks
  • Minimal boilerplate: Focus on business logic, not plumbing
  • Backend agnostic: Works with any API
  • UI flexibility: Bring your own UI or use integrations
  • Enterprise ready: Authentication, access control, audit logs
  • Type safe: Full TypeScript support
  • Extensible: Customize every aspect

Perfect for:

  • Admin panels
  • Internal tools
  • Dashboards
  • Data management applications
  • B2B applications

In the next lesson, we'll explore more data fetching libraries and compare different solutions for various use cases.