How to Structure a Next.js Project for Long-Term Maintainability
Folder structure, feature modules, naming conventions, API layers, and scaling patterns from MVP to production. Practical guide for Next.js dashboard and Web3 projects.
Why folder structure matters
A messy project structure slows every future feature. Developers waste time finding files, duplicate logic across folders, and break imports when refactoring. A clear structure scales from a weekend MVP to a production app with multiple contributors — without reorganization.
Recommended folder structure
src/
app/ # Next.js App Router pages
layout.tsx
page.tsx
dashboard/
page.tsx
layout.tsx
api/
balances/route.ts
components/
ui/ # Shared primitives (Button, Skeleton, Modal)
layout/ # Header, Footer, Sidebar
features/ # Feature modules (self-contained)
portfolio/
components/
hooks/
types.ts
wallet/
components/
hooks/
hooks/ # Shared hooks
lib/ # Config, clients, utilities
wagmi-config.ts
api-client.ts
format.ts
constants.ts
types/ # Global TypeScript types
data/ # Static data, article contentComponents, hooks, utils, services, types
**components/ui/** — Reusable UI primitives with no business logic. Button, Input, Skeleton, Modal, DataTable.
**components/layout/** — Page structure components. Header, Footer, Sidebar, PageContainer.
**features/** — Self-contained feature modules. Each feature owns its components, hooks, and types. A feature should be deletable without breaking unrelated code.
**hooks/** — Shared hooks used across features. useDebounce, useMediaQuery, useLocalStorage.
**lib/** — Configuration, API clients, utility functions, constants. No React components here.
**types/** — Global TypeScript interfaces and type aliases.
API and data layer
// lib/api-client.ts — centralized HTTP layer
type RequestOptions = { params?: Record<string, string>; signal?: AbortSignal };
async function request<T>(path: string, options?: RequestOptions): Promise<T> {
const url = new URL(path, '/api');
if (options?.params) {
Object.entries(options.params).forEach(([k, v]) => url.searchParams.set(k, v));
}
const res = await fetch(url, { signal: options?.signal });
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
export const api = {
portfolio: {
get: (address: string) => request<Portfolio>(`/portfolio/${address}`),
},
prices: {
get: (ids: string[]) => request<PriceMap>('/prices', { params: { ids: ids.join(',') } }),
},
};Feature-based organization
When a feature grows beyond 2 components and 1 hook, give it its own folder:
features/transactions/
components/
TransactionTable.tsx
TransactionRow.tsx
TransactionFilters.tsx
hooks/
useTransactions.ts
useTransactionExport.ts
types.ts
index.ts # Public API — only export what other features need// features/transactions/index.ts
export { TransactionTable } from './components/TransactionTable';
export { useTransactions } from './hooks/useTransactions';
export type { Transaction } from './types';Naming conventions
Files: PascalCase for components (PortfolioSummary.tsx), camelCase for hooks (usePortfolio.ts) and utilities (format.ts). Folders: kebab-case or lowercase (features/portfolio/, components/ui/). Types: PascalCase interfaces (TokenPosition, ApiResponse). Constants: SCREAMING_SNAKE_CASE (SUPPORTED_CHAINS, CACHE_TTL).
Scaling from MVP to production
**MVP (1–2 weeks):** Flat structure is fine. components/, hooks/, lib/, app/.
**Growing (1–3 months):** Introduce features/ folder. Extract shared UI into components/ui/. Centralize API calls in lib/api-client.ts.
**Production (3+ months):** Feature modules with index.ts exports. Shared types in types/. API routes organized by domain. CI linting rules for import boundaries.
FAQ
**Monorepo or single repo?** Single repo for most Web3 dashboards. Monorepo only if you have separate packages (shared UI library, SDK).
**Where do API routes go?** app/api/ following the same domain structure as features. app/api/portfolio/[address]/route.ts.
**Should I use barrel exports (index.ts)?** Yes for feature modules (features/portfolio/index.ts). No for components/ui/ — direct imports are clearer and tree-shake better.
**How do I enforce structure?** ESLint import rules. TypeScript path aliases in tsconfig.json (@/features/*, @/components/*, @/lib/*).
Want to work together? I build Web3 dashboards and DeFi interfaces.