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 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.