ServicesContactCall Us Today
Call Us Today
ServicesContact
Back to blog

March 20, 2025

Building an Admin Panel with Next.js

Patterns for building a secure, well-structured admin panel — route protection, data tables, and role-based access with adminProcedure.

Building an Admin Panel with Next.js

On this page

StructureRoute protectionadminProcedureDataTableKey rules

Structure

The admin panel lives under app/admin/ and is protected at the middleware level. Each resource gets its own folder with a page.tsx and a columns.tsx:

app/admin/
  layout.tsx       ← sidebar + auth guard
  page.tsx         ← overview/dashboard
  users/
    page.tsx
    columns.tsx
  products/
    page.tsx
    columns.tsx
  orders/
    page.tsx
    columns.tsx

Route protection

Guard the entire admin tree in middleware:

// middleware.ts
if (request.nextUrl.pathname.startsWith("/admin")) {
  const session = await auth.api.getSession({ headers: request.headers });
  if (!session?.user || session.user.role !== "admin") {
    return NextResponse.redirect(new URL("/sign-in", request.url));
  }
}

adminProcedure

All admin data goes through tRPC using adminProcedure, which checks the session role server-side before executing:

// services/trpc/init.ts
export const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
  if (ctx.session.user.role !== "admin") {
    throw new TRPCError({ code: "FORBIDDEN" });
  }
  return next({ ctx });
});
// services/trpc/routers/admin/users.ts
export const usersRouter = router({
  list: adminProcedure.query(() => db.query.users.findMany()),
 
  delete: adminProcedure
    .input(z.object({ id: z.string() }))
    .mutation(({ input }) =>
      db.delete(users).where(eq(users.id, input.id))
    ),
});

DataTable

Always use the shared <DataTable> component. Columns go in columns.tsx:

// app/admin/users/columns.tsx
import { ColumnDef } from "@tanstack/react-table";
 
export const columns: ColumnDef<User>[] = [
  { accessorKey: "name", header: "Name" },
  { accessorKey: "email", header: "Email" },
  {
    id: "actions",
    cell: ({ row }) => <UserActions user={row.original} />,
  },
];
// app/admin/users/page.tsx
const UsersPage = () => {
  const trpc = useTRPC();
  const { data, isPending } = useQuery(trpc.admin.users.list.queryOptions());
 
  if (isPending) return <Skeleton className="h-96 w-full" />;
 
  return <DataTable columns={columns} data={data ?? []} />;
};

Key rules

  • Never fetch directly from admin pages — always go through adminProcedure
  • Never share admin routers with public-facing procedures
  • Keep destructive actions behind AlertDialog for confirmation
  • Use Badge for status labels, DropdownMenu for row actions

Let's Get In Touch.

Your laboratory instruments should serve you, not the other way around. We're happy to help you.

Call Us Today Contact Us

Transforming houses into dream homes with quality craftsmanship and exceptional service.

Quick Links

  • Services
  • Privacy Policy

Contact Us

  • +15036066416
  • contact@samadihomerenovation.com

Location

Portland Oregon 97223

Our Services

  • Kitchen Remodeling
  • Bathroom Renovation
  • Doors & Windows
  • Handyman Services
  • Flooring Installation
  • Interior Painting

© 2025 Samadi Home Renovation LLC. All rights reserved.

Licensed & Bonded & Insured