Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services
Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services

Blog Posts

Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services
Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services
Back to all blogs

How to Structure Your Next.js Application

Jan 20, 2024•8 min read

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
1// 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}

Feature-Based Organization

Organize your code by features rather than file types:

typescript
1// 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}

Component Organization

UI Components

Keep basic UI components in components/ui/:

typescript
1// 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}

Feature Components

Place feature-specific components in their respective feature folders:

typescript
1// 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}

Custom Hooks

Organize custom hooks by functionality:

typescript
1// 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}

Type Definitions

Keep your TypeScript types organized:

typescript
1// 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}

Utility Functions

Organize utility functions by purpose:

typescript
1// 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>

API Routes Organization

Structure your API routes logically:

typescript
1// 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}

Environment Configuration

Keep environment variables organized:

typescript
1// 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

Testing Structure

Organize your tests alongside your code:

typescript
1// 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})

Best Practices

1. Use Barrel Exports Sparingly

Avoid index.ts files that re-export everything. Import directly from files:

typescript
1// ❌ 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'

2. Keep Components Small and Focused

Each component should have a single responsibility:

typescript
1// ✅ 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}

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:

  1. Start with new features using the new structure
  2. Gradually move existing code to the new organization
  3. Update imports as you move files
  4. 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.

Related Posts

React Query Implementation and Why Should You Use It

Learn how React Query simplifies data fetching in React applications with automatic caching, background synchronization, and built-in error handling.