How to Build an Admin Dashboard (2026)
Every app needs an admin panel for data management, user support, and analytics. Here's how to build one fast.
Option 1: Refine (Recommended)
Refine is a React framework for building admin panels and internal tools:
npm create refine-app@latest my-admin
Basic CRUD
import { useTable } from '@refinedev/core'
function PostList() {
const { tableProps } = useTable()
return (
<table {...tableProps}>
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{tableProps.dataSource.map(post => (
<tr key={post.id}>
<td>{post.title}</td>
<td>{post.author}</td>
<td>
<button>Edit</button>
<button>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
)
}
Refine handles:
- Routing
- Data fetching
- Forms with validation
- Pagination and filtering
- Authentication
- Authorization
Option 2: Build Custom with shadcn/ui
Data Table
'use client'
import { useQuery } from '@tanstack/react-query'
import { DataTable } from '@/components/ui/data-table'
import { ColumnDef } from '@tanstack/react-table'
const columns: ColumnDef<User>[] = [
{ accessorKey: 'email', header: 'Email' },
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'role', header: 'Role' },
{
id: 'actions',
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>⋮</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => editUser(row.original.id)}>
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={() => deleteUser(row.original.id)}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
]
function UsersPage() {
const { data } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/admin/users').then(r => r.json()),
})
return <DataTable columns={columns} data={data?.users ?? []} />
}
Create/Edit Form
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import * as z from 'zod'
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
role: z.enum(['admin', 'user']),
})
function UserForm({ user }: { user?: User }) {
const form = useForm({
resolver: zodResolver(userSchema),
defaultValues: user,
})
const onSubmit = async (data: z.infer<typeof userSchema>) => {
if (user) {
await fetch(`/api/admin/users/${user.id}`, {
method: 'PATCH',
body: JSON.stringify(data),
})
} else {
await fetch('/api/admin/users', {
method: 'POST',
body: JSON.stringify(data),
})
}
}
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<input {...form.register('name')} placeholder="Name" />
<input {...form.register('email')} placeholder="Email" />
<select {...form.register('role')}>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<button type="submit">Save</button>
</form>
)
}
Charts (Recharts)
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts'
function RevenueChart() {
const { data } = useQuery({
queryKey: ['revenue'],
queryFn: () => fetch('/api/admin/analytics/revenue').then(r => r.json()),
})
return (
<LineChart width={600} height={300} data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="revenue" stroke="#8884d8" />
</LineChart>
)
}
Authentication & Authorization
Middleware
// middleware.ts
import { auth } from '@/lib/auth'
import { NextResponse } from 'next/server'
export async function middleware(req: Request) {
const user = await auth()
if (!user) {
return NextResponse.redirect(new URL('/login', req.url))
}
if (!user.roles.includes('admin')) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
return NextResponse.next()
}
export const config = {
matcher: '/admin/:path*',
}
Role-Based Access Control
function hasPermission(user: User, action: string, resource: string) {
const permissions = {
admin: ['*'],
editor: ['posts:read', 'posts:write'],
viewer: ['posts:read'],
}
return permissions[user.role].includes('*') ||
permissions[user.role].includes(`${resource}:${action}`)
}
// Usage
{hasPermission(user, 'write', 'posts') && (
<button>Create Post</button>
)}
Analytics Dashboard
function DashboardStats() {
const { data: stats } = useQuery({
queryKey: ['stats'],
queryFn: () => fetch('/api/admin/stats').then(r => r.json()),
})
return (
<div className="grid grid-cols-4 gap-4">
<Card>
<CardTitle>Total Users</CardTitle>
<CardContent>{stats?.totalUsers}</CardContent>
</Card>
<Card>
<CardTitle>Active Subscriptions</CardTitle>
<CardContent>{stats?.activeSubscriptions}</CardContent>
</Card>
<Card>
<CardTitle>MRR</CardTitle>
<CardContent>${stats?.mrr}</CardContent>
</Card>
<Card>
<CardTitle>Churn Rate</CardTitle>
<CardContent>{stats?.churnRate}%</CardContent>
</Card>
</div>
)
}
Search & Filtering
function UserTable() {
const [filters, setFilters] = useState({
search: '',
role: 'all',
status: 'all',
})
const { data } = useQuery({
queryKey: ['users', filters],
queryFn: () => fetch(`/api/admin/users?${new URLSearchParams(filters)}`).then(r => r.json()),
})
return (
<>
<div className="filters">
<input
placeholder="Search..."
value={filters.search}
onChange={e => setFilters(prev => ({ ...prev, search: e.target.value }))}
/>
<select
value={filters.role}
onChange={e => setFilters(prev => ({ ...prev, role: e.target.value }))}
>
<option value="all">All Roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
</div>
<DataTable data={data?.users ?? []} />
</>
)
}
FAQ
Refine vs React Admin?
Refine is more flexible and modern. React Admin is older, more opinionated. Both are excellent. Start with Refine.
Should I build custom or use a framework?
Use Refine for typical admin panels (users, content, settings). Build custom when you need unique workflows or heavy customization.
How do I secure my admin panel?
Require authentication, check roles in middleware, log all admin actions, and use HTTPS in production.
Bottom Line
Use Refine for fast admin panel development. Build custom with shadcn/ui + TanStack Table when you need full control. Protect with authentication middleware. Add analytics, search, and CRUD for the entities you need to manage.