5+ years web developer
5+ years web developer
5+ years web developer
5+ years web developer
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.
A well-organized project structure is crucial for:
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
With Next.js 13+ App Router, organize your routes logically:
typescript1// app/layout.tsx 2export default function RootLayout({ 3 children, 4}: { 5 children: React.ReactNode 6}) { 7 return ( 8 <html lang="en"> 9 <body> 10 <Header /> 11 <main>{children}</main> 12 <Footer /> 13 </body> 14 </html> 15 ) 16} 17 18// app/page.tsx 19export default function HomePage() { 20 return <HomePageContent /> 21}
Organize your code by features rather than file types:
typescript1// features/auth/components/LoginForm.tsx 2export function LoginForm() { 3 return ( 4 <form> 5 {/* Login form implementation */} 6 </form> 7 ) 8} 9 10// features/auth/hooks/useAuth.ts 11export function useAuth() { 12 // Authentication logic 13} 14 15// features/auth/types/auth.types.ts 16export interface User { 17 id: string 18 email: string 19 name: string 20}
Keep basic UI components in components/ui/
:
typescript1// components/ui/Button.tsx 2interface ButtonProps { 3 variant?: 'primary' | 'secondary' 4 size?: 'sm' | 'md' | 'lg' 5 children: React.ReactNode 6} 7 8export function Button({ variant = 'primary', size = 'md', children }: ButtonProps) { 9 return ( 10 <button className={`btn btn-${variant} btn-${size}`}> 11 {children} 12 </button> 13 ) 14}
Place feature-specific components in their respective feature folders:
typescript1// features/dashboard/components/DashboardStats.tsx 2export function DashboardStats() { 3 return ( 4 <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> 5 {/* Stats implementation */} 6 </div> 7 ) 8}
Organize custom hooks by functionality:
typescript1// hooks/useLocalStorage.ts 2export function useLocalStorage<T>(key: string, initialValue: T) { 3 const [storedValue, setStoredValue] = useState<T>(() => { 4 try { 5 const item = window.localStorage.getItem(key) 6 return item ? JSON.parse(item) : initialValue 7 } catch (error) { 8 return initialValue 9 } 10 }) 11 12 const setValue = (value: T | ((val: T) => T)) => { 13 try { 14 const valueToStore = value instanceof Function ? value(storedValue) : value 15 setStoredValue(valueToStore) 16 window.localStorage.setItem(key, JSON.stringify(valueToStore)) 17 } catch (error) { 18 console.error(error) 19 } 20 } 21 22 return [storedValue, setValue] as const 23}
Keep your TypeScript types organized:
typescript1// types/api.types.ts 2export interface ApiResponse<T> { 3 data: T 4 message: string 5 success: boolean 6} 7 8export interface PaginatedResponse<T> extends ApiResponse<T[]> { 9 pagination: { 10 page: number 11 limit: number 12 total: number 13 totalPages: number 14 } 15} 16 17// types/user.types.ts 18export interface User { 19 id: string 20 email: string 21 name: string 22 avatar?: string 23 createdAt: string 24 updatedAt: string 25}
Organize utility functions by purpose:
typescript1// lib/utils.ts 2export function cn(...inputs: ClassValue[]) { 3 return twMerge(clsx(inputs)) 4} 5 6export function formatDate(date: string | Date) { 7 return new Intl.DateTimeFormat('en-US', { 8 year: 'numeric', 9 month: 'long', 10 day: 'numeric', 11 }).format(new Date(date)) 12} 13 14// lib/validations.ts 15import { z } from 'zod' 16 17export const loginSchema = z.object({ 18 email: z.string().email('Invalid email address'), 19 password: z.string().min(6, 'Password must be at least 6 characters'), 20}) 21 22export type LoginFormData = z.infer<typeof loginSchema>
Structure your API routes logically:
typescript1// app/api/auth/login/route.ts 2export async function POST(request: Request) { 3 try { 4 const body = await request.json() 5 const validatedData = loginSchema.parse(body) 6 7 // Authentication logic 8 const user = await authenticateUser(validatedData) 9 10 return NextResponse.json({ user }, { status: 200 }) 11 } catch (error) { 12 return NextResponse.json( 13 { error: 'Invalid credentials' }, 14 { status: 401 } 15 ) 16 } 17}
Keep environment variables organized:
typescript1// lib/config.ts 2export const config = { 3 app: { 4 name: process.env.NEXT_PUBLIC_APP_NAME || 'My App', 5 url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000', 6 }, 7 database: { 8 url: process.env.DATABASE_URL!, 9 }, 10 auth: { 11 secret: process.env.AUTH_SECRET!, 12 providers: { 13 google: { 14 clientId: process.env.GOOGLE_CLIENT_ID!, 15 clientSecret: process.env.GOOGLE_CLIENT_SECRET!, 16 }, 17 }, 18 }, 19} as const
Organize your tests alongside your code:
typescript1// components/ui/Button.test.tsx 2import { render, screen } from '@testing-library/react' 3import { Button } from './Button' 4 5describe('Button', () => { 6 it('renders with correct text', () => { 7 render(<Button>Click me</Button>) 8 expect(screen.getByText('Click me')).toBeInTheDocument() 9 }) 10})
Avoid index.ts files that re-export everything. Import directly from files:
typescript1// ❌ Avoid 2import { Button, Input, Card } from '@/components' 3 4// ✅ Prefer 5import { Button } from '@/components/ui/Button' 6import { Input } from '@/components/ui/Input' 7import { Card } from '@/components/ui/Card'
Each component should have a single responsibility:
typescript1// ✅ Good - Single responsibility 2export function UserAvatar({ user }: { user: User }) { 3 return ( 4 <img 5 src={user.avatar || '/default-avatar.png'} 6 alt={user.name} 7 className="w-8 h-8 rounded-full" 8 /> 9 ) 10} 11 12// ❌ Avoid - Multiple responsibilities 13export function UserProfile({ user }: { user: User }) { 14 return ( 15 <div> 16 <img src={user.avatar} alt={user.name} /> 17 <h1>{user.name}</h1> 18 <p>{user.email}</p> 19 <button>Edit Profile</button> 20 <button>Delete Account</button> 21 </div> 22 ) 23}
Keep related files together:
features/auth/
├── components/
│ ├── LoginForm.tsx
│ └── SignupForm.tsx
├── hooks/
│ └── useAuth.ts
├── types/
│ └── auth.types.ts
└── utils/
└── auth.utils.ts
If you're refactoring an existing project:
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.