← Back to articles

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.

Get AI tool guides in your inbox

Weekly deep-dives on the best AI coding tools, automation platforms, and productivity software.