NextAdmin follows a modular, domain-driven architecture that separates concerns and scales cleanly as your application grows. The codebase is organized so both developers and AI coding agents can navigate it intuitively.
Directory structure
src/
├── app/ # Next.js App Router
│ ├── layout.tsx # Root layout (ThemeProvider + Providers + Toaster)
│ ├── providers.tsx # Client wrapper for contexts
│ ├── (with-layouts)/ # Pages WITH app shell (sidebar + header)
│ │ ├── layout.tsx # App shell: collapsible sidebar + mobile sheet
│ │ ├── (dashboard)/ # Dashboard pages: Home / analytics / CRM / etc.
│ │ └── ...
│ └── (without-layouts)/ # Pages WITHOUT app shell
├── components/
│ ├── common/ # Shared UI Components: Header, Sidebar etc.
│ └── tailgrids/ # Design-System primitives (Button, Card, Sheet, Dialog, …)
├── services/ # Manage services
│ ├── api/ # Mock REST layer
│ │ ├── home/
│ │ └── ...
├── hooks/ # Hooks
├── types/ # Shared TypeScript types
├── utils/ # Helper functions
│ ├── cn.ts
│ └── ...Key Directories
- app/ - Next.js App Router
- (with-layouts)/ - Pages WITH app shell (sidebar + header)
- (without-layouts)/ - Pages WITHOUT app shell
- components/ - Shared UI Components
- services/api/ - Mock REST layer with domain-specific data and types
- hooks/ - Hooks
- utils/ - Helper functions
- types/ - Shared TypeScript types
API Layer
All the API calls are made through service functions located under services/api/<page-name>/. Each domain under services/api/<page-name>/ follows the same shape:
services/api/<page-name>/
├── data.ts # Raw mock data (Optionally added for mock data source)
├── index.ts # API requests
└── types.ts # Request / Response typesArchitectural patterns
1. App shell
The application has two layout groups in app directory: (with-layouts) and (without-layouts).
/app/(with-layouts)/layout.tsxhasmax-w-384(≈1536px) shell width with a distinctive "card within a card" framing for page content. Both desktop sidebar and mobileSheetare rendered at once, with one hidden via breakpoint./app/(without-layouts)/layout.tsxhas no shell width and expands for full page content.
2. Co-located Sub-components
A page and it's sub-components are isolated into same place.
<page-name>/
├── page.tsx # Route entry
├── _component/ # Components
│ └── <feature>/ # A single component
│ ├── index.tsx # Main Component Export
│ ├── <sub-component>.tsx # Sub Component
│ ├── icons.tsx # Icons required for the component
│ ├── skeletons.tsx # Skeletons required for the component
│ ├── types.ts # Types for the component
│ └── utils.ts # Optional for utils3. Mock API as a service layer
Next Admin is designed to be integrated with any backend. Current implementation uses a mock REST layer which looks something like this:
// services/api/manage-team/index.ts
export async function getTeamMembers(): Promise<TeamMember[]> {
await delay();
return teamMembers;
}But it can easily be replaced with a real API calls using fetch or axios.
// services/api/manage-team/index.ts
export async function getTeamMembers(): Promise<TeamMember[]> {
const { data } = await axios.get("/api/manage-team");
return data;
}/api-integration skill can be used to replace the mock API with a real API.
4. Tanstack Query and response mapping
API calls are made using Tanstack Query and gets mapped to the expected shape before rendering, so that you or your AI Agent can't mess up with the UI. The data flow is as follows:
services/api/<domain> → useQuery hook in component → response mapper → rendered UIExample:
// services/api/manage-team/index.ts
export async function getTeamMembers(): Promise<TeamMember[]> {
await delay();
return teamMembers;
}
// _component/manage-team/utils.ts
export async function getTeamMembersResponseMapper(teamMembers: TeamMember[]): Promise<TeamMemberResponse[]> {
return teamMembers.map((teamMember) => ({
id: teamMember.id,
name: teamMember.name,
email: teamMember.email,
role: teamMember.role,
}));
}
// _component/manage-team/index.tsx
export default function ManageTeam() {
const { data, isLoading } = useQuery({
queryKey: ["manage-team"],
queryFn: getTeamMembers,
});
if (isLoading) {
return <ManageTeamSkeleton />;
}
const teamMembers = data ? getTeamMembersResponseMapper(data) : undefined;
// ...
}
