# Route group or path segment: how to lay out an admin section

> A route group shares a layout without touching the URL; a path segment is the URL. Admin panels need both, and confusing them produces public pages and 404s.

`src/app/(admin)/admin/users/page.tsx` looks redundant the first time you see
it. Why is "admin" in the path twice? Deleting either copy breaks something, and
which thing breaks depends on which copy you delete, so it is worth
understanding what each one does.

## The two mechanisms

**A path segment** is a folder whose name appears in the URL.
`src/app/admin/users/page.tsx` serves `/admin/users`.

**A route group** is a folder whose name is in parentheses. It is invisible to
the URL and exists to give a set of routes a shared layout, or to keep them out
of another layout. `src/app/(admin)/dashboard/page.tsx` serves `/dashboard`,
with the layout from `(admin)`.

So in `src/app/(admin)/admin/users/page.tsx`:

- `(admin)` supplies `layout.tsx`, and with it the role check and the shell.
- `admin` is the first URL segment.
- The URL is `/admin/users`.

## What each mistake produces

**Delete the group; keep the segment.** `src/app/admin/users/page.tsx` serves
the same URL and inherits only the root layout. The layout's role check and
the sidebar are gone. Unless the page checks the role itself, it is public and
nothing warns you. This is the dangerous one, and the reason every admin page
should call the guard too.

**Delete the segment; keep the group.** `src/app/(admin)/users/page.tsx` serves
`/users`, not `/admin/users`. Every link 404s, and the page you meant to hide
behind `/admin` is now at a top-level URL that a proxy matcher on `/admin/*`
does not cover.

**Use only a segment and put the layout in it.**
`src/app/admin/layout.tsx` plus `src/app/admin/users/page.tsx` works perfectly
and is a legitimate design. The reason to prefer the group is that it can hold
routes that are *not* under `/admin` (a `/impersonate` route, a `/support`
console) while still sharing the layout and the check. If you are certain
everything will live under one prefix, the plain segment is simpler.

## Why the group is the better default here

**The layout is where the boundary lives.** A group makes it obvious that the
grouping *is* the boundary: everything inside `(admin)` is admin. A page moved
out of the folder loses protection, and the folder name is the reminder.

**Route groups can be added without breaking URLs.** Wrapping an existing
`admin/` folder in `(admin)/` changes no URL and no link. Adding a segment
changes every URL.

**Two groups can share a URL space.** `(marketing)/page.tsx` and
`(app)/dashboard/page.tsx` can have completely different chrome without a URL
prefix distinguishing them.

## Rules that avoid the traps

- **One `page.tsx` per URL.** Two groups both defining `page.tsx` for `/` is a
  build error, and the message names the conflict but not which one you meant
  to keep.
- **A group is not a segment in `usePathname()`.** Highlighting the active nav
  item compares against `/admin/users`, never `/(admin)/admin/users`.
- **A group is not a segment in a proxy matcher either.** Match `/admin/:path*`.
- **`LayoutProps` and `PageProps` are generated per URL route.** A group's
  layout has the same route literal as the layout above it, which is why the
  `(admin)` layout types `children` by hand instead of using
  `LayoutProps<"/">`.
- **Loading and error boundaries follow the folder, not the URL.**
  `(admin)/loading.tsx` covers everything in the group, which is usually what
  you want for an admin panel: one skeleton, one error page.

## A layout that works for the whole section

```tsx
// src/app/(admin)/layout.tsx
export default async function AdminLayout({ children }: { children: ReactNode }) {
  const admin = await requireRole("admin", "/admin");
  return <AdminShell user={toShellUser(admin)}>{children}</AdminShell>;
}
```

The layout keeps non-admins out of the shell. It does not re-run on client
navigation, so each page in the group calls the same `requireRole` again with
its own path; with the session read wrapped in React `cache`, that costs
nothing.

Add `error.tsx` and `not-found.tsx` inside the group (next to the layout, or
in `(admin)/admin/`). Without them, an admin page that throws renders the
app-wide error page and the operator loses the navigation they were using, a
small thing that is very annoying at 2am.

## When to add a second group

The signal is a page that needs a different shell: a full-screen impersonation
banner, a print view of an invoice, an embedded report with no chrome. Rather
than adding conditionals to `AdminShell`, add `(admin-bare)` with its own
minimal layout, and give it the same role check, because the group is the
boundary and a new group is a new boundary that starts empty.

## Checking your work

- Every file under `(admin)` renders inside the shell, and every URL it serves
  starts with `/admin`.
- Deleting the group folder name from a path is a change you would catch in
  review, because you know what it does now.
- `usePathname()` never contains parentheses.
- Signed out, every URL in the section redirects to sign-in.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
