Building a scalable Next.js application requires careful planning and organization. In this guide, I'll walk you through the best practices for structuring your Next.js project to ensure maintainability, scalability, and developer experience.
Why Project Structure Matters
A well-organized project structure is crucial for:
- Maintainability: Easy to find and modify code
- Scalability: Can grow with your team and requirements
- Developer Experience: Faster development and onboarding
- Code Reusability: Shared components and utilities
- Testing: Organized test files and clear boundaries
Recommended Next.js Project Structure
Here's the folder structure I recommend for most Next.js applications:
src/
├── app/ # App Router (Next.js 13+)
│ ├── (auth)/ # Route groups
│ ├── api/ # API routes
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ └── page.tsx # Home page
├── components/ # Shared components
│ ├── ui/ # Basic UI components
│ ├── forms/ # Form components
│ └── layout/ # Layout components
├── features/ # Feature-based organization
│ ├── auth/ # Authentication feature
│ ├── dashboard/ # Dashboard feature
│ └── blog/ # Blog feature
├── lib/ # Utilities and configurations
│ ├── utils.ts # Helper functions
│ ├── validations.ts # Zod schemas
│ └── constants.ts # App constants
├── hooks/ # Custom React hooks
├── types/ # TypeScript type definitions
├── styles/ # Additional styles
└── public/ # Static assets
App Router Structure
With Next.js 13+ App Router, organize your routes logically:
typescript// app/layout.tsx export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <Header /> <main>{children}</main> <Footer /> </body> </html> ) } // app/page.tsx export default function HomePage() { return <HomePageContent /> }
Feature-Based Organization
Organize your code by features rather than file types:
typescript// features/auth/components/LoginForm.tsx export function LoginForm() { return ( <form> {/* Login form implementation */} </form> ) } // features/auth/hooks/useAuth.ts export function useAuth() { // Authentication logic } // features/auth/types/auth.types.ts export interface User { id: string email: string name: string }
Component Organization
UI Components
Keep basic UI components in components/ui/:
typescript// components/ui/Button.tsx interface ButtonProps { variant?: 'primary' | 'secondary' size?: 'sm' | 'md' | 'lg' children: React.ReactNode } export function Button({ variant = 'primary', size = 'md', children }: ButtonProps) { return ( <button className={`btn btn-${variant} btn-${size}`}> {children} </button> ) }
Feature Components
Place feature-specific components in their respective feature folders:
typescript// features/dashboard/components/DashboardStats.tsx export function DashboardStats() { return ( <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> {/* Stats implementation */} </div> ) }
Custom Hooks
Organize custom hooks by functionality:
typescript// hooks/useLocalStorage.ts export function useLocalStorage<T>(key: string, initialValue: T) { const [storedValue, setStoredValue] = useState<T>(() => { try { const item = window.localStorage.getItem(key) return item ? JSON.parse(item) : initialValue } catch (error) { return initialValue } }) const setValue = (value: T | ((val: T) => T)) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value setStoredValue(valueToStore) window.localStorage.setItem(key, JSON.stringify(valueToStore)) } catch (error) { console.error(error) } } return [storedValue, setValue] as const }
Type Definitions
Keep your TypeScript types organized:
typescript// types/api.types.ts export interface ApiResponse<T> { data: T message: string success: boolean } export interface PaginatedResponse<T> extends ApiResponse<T[]> { pagination: { page: number limit: number total: number totalPages: number } } // types/user.types.ts export interface User { id: string email: string name: string avatar?: string createdAt: string updatedAt: string }
Utility Functions
Organize utility functions by purpose:
typescript// lib/utils.ts export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } export function formatDate(date: string | Date) { return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric', }).format(new Date(date)) } // lib/validations.ts import { z } from 'zod' export const loginSchema = z.object({ email: z.string().email('Invalid email address'), password: z.string().min(6, 'Password must be at least 6 characters'), }) export type LoginFormData = z.infer<typeof loginSchema>
API Routes Organization
Structure your API routes logically:
typescript// app/api/auth/login/route.ts export async function POST(request: Request) { try { const body = await request.json() const validatedData = loginSchema.parse(body) // Authentication logic const user = await authenticateUser(validatedData) return NextResponse.json({ user }, { status: 200 }) } catch (error) { return NextResponse.json( { error: 'Invalid credentials' }, { status: 401 } ) } }
Environment Configuration
Keep environment variables organized:
typescript// lib/config.ts export const config = { app: { name: process.env.NEXT_PUBLIC_APP_NAME || 'My App', url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000', }, database: { url: process.env.DATABASE_URL!, }, auth: { secret: process.env.AUTH_SECRET!, providers: { google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }, }, }, } as const
Testing Structure
Organize your tests alongside your code:
typescript// components/ui/Button.test.tsx import { render, screen } from '@testing-library/react' import { Button } from './Button' describe('Button', () => { it('renders with correct text', () => { render(<Button>Click me</Button>) expect(screen.getByText('Click me')).toBeInTheDocument() }) })
Best Practices
1. Use Barrel Exports Sparingly
Avoid index.ts files that re-export everything. Import directly from files:
typescript// ❌ Avoid import { Button, Input, Card } from '@/components' // ✅ Prefer import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { Card } from '@/components/ui/Card'
2. Keep Components Small and Focused
Each component should have a single responsibility:
typescript// ✅ Good - Single responsibility export function UserAvatar({ user }: { user: User }) { return ( <img src={user.avatar || '/default-avatar.png'} alt={user.name} className="w-8 h-8 rounded-full" /> ) } // ❌ Avoid - Multiple responsibilities export function UserProfile({ user }: { user: User }) { return ( <div> <img src={user.avatar} alt={user.name} /> <h1>{user.name}</h1> <p>{user.email}</p> <button>Edit Profile</button> <button>Delete Account</button> </div> ) }
3. Use Consistent Naming Conventions
- Components: PascalCase (UserProfile)
- Hooks: camelCase starting with 'use' (useAuth)
- Utilities: camelCase (formatDate)
- Types: PascalCase (User, ApiResponse)
4. Group Related Files
Keep related files together:
features/auth/
├── components/
│ ├── LoginForm.tsx
│ └── SignupForm.tsx
├── hooks/
│ └── useAuth.ts
├── types/
│ └── auth.types.ts
└── utils/
└── auth.utils.ts
Migration Strategy
If you're refactoring an existing project:
- Start with new features using the new structure
- Gradually move existing code to the new organization
- Update imports as you move files
- Remove old files once everything is migrated
Conclusion
A well-structured Next.js application is easier to maintain, scale, and work with. By following these patterns and organizing your code logically, you'll create a foundation that supports long-term growth and team collaboration.
Remember: The best structure is the one that works for your team and project. Start with these guidelines and adapt them to your specific needs.
For more Next.js best practices and advanced patterns, check out the official Next.js documentation.