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;
// ...
}
