Skip to content

Improver2108/nextjs-architecture

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 

Repository files navigation

πŸ—οΈ Complete Next.js Architecture & Module Independence Guide

Your definitive guide to building a scalable, maintainable Next.js application with enforced architectural boundaries


πŸ“‹ Table of Contents

  1. Introduction
  2. The Big Picture
  3. Folder Structure Deep Dive
  4. Module Independence Rules
  5. The Barrel File Problem & Solution
  6. Next.js Configuration Deep Dive
  7. TypeScript Configuration Explained
  8. ESLint Configuration Deep Dive
  9. Independent Module Configuration
  10. Workflow Guide
  11. Dos and Don'ts
  12. Real-World Examples
  13. Troubleshooting
  14. Configuration Files Reference

🎯 Introduction

Welcome to the complete architecture documentation! This isn't just another boring config file explanation. We're building something special hereβ€”a codebase that scales without turning into spaghetti.

Why This Matters

Imagine you're building a house. You wouldn't want the kitchen plumbing connected to the bedroom electrical system, right? The same principle applies to our code. We're creating independent modules that:

  • βœ… Can be understood in isolation
  • βœ… Won't break when other parts change
  • βœ… Keep your build times fast (yes, really!)
  • βœ… Make code reviews actually enjoyable

What Makes This Architecture Special

This isn't just folder organizationβ€”it's a complete system with three layers of enforcement:

  1. TypeScript - Provides type safety and path aliases
  2. ESLint - Enforces import rules at development time
  3. Next.js Config - Optimizes bundle size at build time

Think of it like building a car with:

  • πŸ”§ TypeScript = The blueprint (what goes where)
  • 🚦 ESLint = The traffic rules (what can connect to what)
  • 🏎️ Next.js = The engine optimization (making it fast)

🎨 The Big Picture

Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      🏠 APP FOLDER                           β”‚
β”‚  Pages, layouts, route handlers - your application entry    β”‚
β”‚                                                              β”‚
β”‚  CAN IMPORT FROM:                                           β”‚
β”‚  β€’ Shared folders (components, utils, config)              β”‚
β”‚  β€’ UI components                                            β”‚
β”‚  β€’ Feature INDEX files ONLY (src/features/*/index.ts)      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    🎯 FEATURES                               β”‚
β”‚  Business logic organized by domain (auth, dashboard, etc)  β”‚
β”‚                                                              β”‚
β”‚  CAN IMPORT FROM:                                           β”‚
β”‚  β€’ Its own family members (same feature)                    β”‚
β”‚  β€’ Shared folders                                           β”‚
β”‚  β€’ UI components                                            β”‚
β”‚  ❌ CANNOT import from other features                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    πŸ”§ SHARED FOLDERS                         β”‚
β”‚  Pure utilities, configs, types - the foundation            β”‚
β”‚                                                              β”‚
β”‚  CAN IMPORT FROM:                                           β”‚
β”‚  β€’ Other shared folders                                     β”‚
β”‚  β€’ UI components                                            β”‚
β”‚  ❌ CANNOT import from features                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    🎨 UI LAYER                               β”‚
β”‚  Pure presentational components (shadcn/ui, styles)         β”‚
β”‚                                                              β”‚
β”‚  CAN IMPORT FROM:                                           β”‚
β”‚  β€’ Other UI components only                                 β”‚
β”‚  ❌ Most restricted layer - zero business logic             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The Three-Tier Enforcement System

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Layer 1: TypeScript Configuration (tsconfig.json)     β”‚
β”‚  πŸ“ Enables: Path aliases, module resolution            β”‚
β”‚  🎯 Purpose: Makes imports clean and consistent         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Layer 2: ESLint Configuration (eslint.config.mjs)     β”‚
β”‚  🚦 Enforces: Import rules, architectural boundaries    β”‚
β”‚  🎯 Purpose: Catches violations during development      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Layer 3: Next.js Configuration (next.config.ts)       β”‚
β”‚  🏎️ Optimizes: Bundle size, tree-shaking               β”‚
β”‚  🎯 Purpose: Ensures fast builds and small bundles      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“ Folder Structure Deep Dive

The Complete Structure

src/
β”œβ”€β”€ πŸ“± app/                    # Next.js App Router
β”‚   β”œβ”€β”€ page.tsx
β”‚   β”œβ”€β”€ layout.tsx
β”‚   β”œβ”€β”€ global.css             # ⭐ Special: Can be imported by app layer
β”‚   └── [routes]/
β”‚
β”œβ”€β”€ 🎯 features/               # Feature modules (your business logic)
β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”œβ”€β”€ index.ts          # ⭐ Public API (the ONLY file app/ can import)
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”œβ”€β”€ utils/
β”‚   β”‚   └── types.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ dashboard/
β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   └── ...
β”‚   β”‚
β”‚   └── profile/
β”‚       β”œβ”€β”€ index.ts
β”‚       └── ...
β”‚
β”œβ”€β”€ πŸ”§ components/
β”‚   β”œβ”€β”€ shared/               # Shared business components
β”‚   └── ui/                   # Pure UI components (shadcn/ui)
β”‚
β”œβ”€β”€ πŸ“š lib/                   # Third-party integrations & utilities
β”‚   β”œβ”€β”€ utils.ts              # ⭐ Part of UI layer
β”‚   β”œβ”€β”€ prisma/               # Database client
β”‚   └── stripe/               # Payment integration
β”‚
β”œβ”€β”€ βš™οΈ config/                # App configuration
β”œβ”€β”€ 🎭 types/                 # Shared TypeScript types
β”œβ”€β”€ πŸ“¦ store/                 # State management (Zustand, Redux, etc)
β”œβ”€β”€ πŸ› οΈ utils/                 # Helper functions
└── 🎨 styles/                # Global styles

Layer Classification

Folder Layer Can Import From
app/ Application Everything (via public APIs)
features/ Feature Own feature + Shared + UI
components/shared/, lib/*/, config/, types/, store/, utils/ Shared Shared + UI
components/ui/, styles/, lib/utils.ts UI UI only

πŸ”’ Module Independence Rules

The Four Layers & Their Rules

Layer 1️⃣: UI Components (Most Restricted)

// components/ui/badge.tsx

// βœ… CAN import from UI layer
import { cn } from "@/lib/utils"; // lib/utils.ts is UI layer
import "./badge.css";

// βœ… CAN import other UI components
import { Button } from "@/components/ui/button";

// ❌ CANNOT import from anywhere else
import { useAuth } from "@/features/auth"; // ❌
import { API_URL } from "@/config/api"; // ❌
import { UserCard } from "@/components/shared/UserCard"; // ❌

Why? UI components should be design system primitives. They're the building blocks, not the buildings.

Real-world analogy: Think of LEGO bricks. A basic brick doesn't know about spaceships or castlesβ€”it's just a brick that can be used anywhere.


Layer 2️⃣: Shared Folders

// components/shared/DataTable.tsx

// βœ… CAN import from shared layer
import { formatDate } from "@/utils/date";
import { API_URL } from "@/config/api";
import { useAppStore } from "@/store/app";

// βœ… CAN import from UI layer
import { Table } from "@/components/ui/table";
import { Button } from "@/components/ui/button";

// ❌ CANNOT import from features
import { useProducts } from "@/features/products"; // ❌

Why? Shared code is foundational. If it depends on features, it's not really sharedβ€”it's coupled.

Real-world analogy: Think of a toolbox. Your hammer doesn't know whether it's building a house or a birdhouse. It's a generic tool that works everywhere.


Layer 3️⃣: Features

// features/products/hooks/useProducts.ts

// βœ… CAN import from its own feature (family)
import { Product } from "../types";
import { fetchProducts } from "../api/productService";
import { ProductCard } from "../components/ProductCard";

// βœ… CAN import from shared layer
import { useDebounce } from "@/utils/hooks";
import { API_URL } from "@/config/api";
import { useAppStore } from "@/store/app";

// βœ… CAN import from UI layer
import { Button } from "@/components/ui/button";

// ❌ CANNOT import from OTHER features
import { useAuth } from "@/features/auth"; // ❌ NOPE!
import { DashboardHeader } from "@/features/dashboard"; // ❌ NOPE!

Why? Features should be independent. If products needs auth, you have two choices:

  1. Move the common logic to shared/
  2. Pass the data down from the app layer

Real-world analogy: Think of departments in a company. The Marketing department doesn't directly access HR's filesβ€”they go through official channels (the app layer).


Layer 4️⃣: App Folder (Most Permissive)

// app/products/page.tsx

// βœ… CAN import feature PUBLIC APIs (index.ts only)
import { ProductList } from "@/features/products";
import { useAuth } from "@/features/auth";

// βœ… CAN import shared layer
import { Container } from "@/components/shared/Container";
import { formatPrice } from "@/utils/format";

// βœ… CAN import UI layer
import { Button } from "@/components/ui/button";

// βœ… CAN import global styles
import "@/app/global.css";

// ❌ CANNOT import feature internals
import { ProductCard } from "@/features/products/components/ProductCard"; // ❌
import { validateProduct } from "@/features/products/utils/validation"; // ❌

Why? The app layer orchestrates features. It's like a conductorβ€”it doesn't play the instruments, it coordinates them.

Real-world analogy: Think of a restaurant manager. They don't cook the food or wash dishesβ€”they coordinate the kitchen, servers, and customers.


🌳 The Barrel File Problem & Solution

The Problem: Barrel Files Can Kill Performance

What's a barrel file? A file that re-exports everything:

// features/products/index.ts (BAD BARREL)
export * from "./components";
export * from "./hooks";
export * from "./utils";
export * from "./api";
export * from "./types";

Why is this bad?

When you import ONE thing:

import { ProductList } from "@/features/products";

Without optimization, Next.js bundles EVERYTHING from that feature:

  • All 15 components (even unused ones)
  • All hooks
  • API functions
  • Utilities
  • Types

Result: Your 50KB page becomes 500KB! πŸ“ˆπŸ’₯

Real-World Impact

Scenario: You have a feature with these exports:

// features/products/index.ts
export { ProductList } from "./components/ProductList"; // 15KB
export { ProductGrid } from "./components/ProductGrid"; // 20KB
export { ProductCard } from "./components/ProductCard"; // 10KB
export { ProductDetail } from "./components/ProductDetail"; // 25KB
export { useProducts } from "./hooks/useProducts"; // 8KB
export { useProductSearch } from "./hooks/useProductSearch"; // 12KB
export { productApi } from "./api/productService"; // 30KB

Without optimization:

// app/products/page.tsx
import { ProductList } from "@/features/products";

// Bundles: ALL 120KB of the feature! πŸ’₯

With optimization:

// app/products/page.tsx
import { ProductList } from "@/features/products";

// Bundles: Only ProductList (15KB) + its dependencies ✨

The Solution: Next.js Package Optimization

In next.config.ts, we automatically optimize all feature imports:

const nextConfig: NextConfig = {
  experimental: {
    optimizePackageImports: [
      "@/features/auth",
      "@/features/products",
      "@/features/dashboard",
      // ... all features automatically added
    ],
  },
};

What this does:

✨ Magic! Next.js now treats your features like external packages:

  • Only bundles what you actually import
  • Tree-shaking works properly
  • Dead code elimination
  • Build size stays lean

Before optimization:

Page                              Size     First Load JS
β”Œ β—‹ /products                    450 kB         525 kB

After optimization:

Page                              Size     First Load JS
β”Œ β—‹ /products                    45 kB          120 kB

That's a 10x improvement! πŸš€


Best Practices for Barrel Files

βœ… DO: Be selective in your exports

// features/products/index.ts (GOOD)
export { ProductList } from "./components/ProductList";
export { ProductCard } from "./components/ProductCard";
export { useProducts } from "./hooks/useProducts";
export type { Product, ProductFilter } from "./types";

// Keep internals private!
// validation.ts, productService.ts, etc. are NOT exported

❌ DON'T: Export everything blindly

// features/products/index.ts (BAD)
export * from "./components"; // Exports 20 components
export * from "./hooks"; // Even internal hooks
export * from "./utils"; // Private utilities leaked!

🎯 The Rule: Only export what the app layer needs. Keep internals private.


βš™οΈ Next.js Configuration Deep Dive

Understanding next.config.ts

import type { NextConfig } from "next";
import bundleAnalyzer from "@next/bundle-analyzer";
import path from "path";
import { readdirSync } from "fs";

const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === "true",
});

const featuresDir = path.resolve(__dirname, "src/features");
const featureFolders = readdirSync(featuresDir).map(
  (folder) => `@/features/${folder}`
);

const nextConfig: NextConfig = {
  reactStrictMode: true,

  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "randomuser.me",
      },
    ],
  },

  experimental: {
    optimizePackageImports: [...featureFolders],
  },
};

export default withBundleAnalyzer(nextConfig);

Configuration Breakdown

1. Bundle Analyzer Setup

const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === "true",
});

Purpose: Visualize your bundle size and identify bloat.

Usage:

ANALYZE=true npm run build

This opens an interactive treemap showing exactly what's in your bundles.

What you'll see:

  • πŸ“Š Size of each module
  • 🎯 Which features are biggest
  • πŸ” Duplicate dependencies
  • πŸ’‘ Optimization opportunities

2. Dynamic Feature Discovery

const featuresDir = path.resolve(__dirname, "src/features");
const featureFolders = readdirSync(featuresDir).map(
  (folder) => `@/features/${folder}`
);

What this does:

  1. Reads src/features at build time
  2. Finds all folders: ['auth', 'products', 'dashboard', ...]
  3. Converts to path aliases: ['@/features/auth', '@/features/products', ...]
  4. Passes to optimizePackageImports

Why this is brilliant:

  • βœ… Add a new feature? Automatically optimized
  • βœ… Rename a feature? No config changes needed
  • βœ… Delete a feature? Automatically removed
  • βœ… Zero maintenance

Is this safe for production? βœ… YES! Because:

  • Runs at build time (not runtime)
  • src/features is part of your repository
  • Works on all platforms (Vercel, Netlify, Docker, etc.)
  • No filesystem dependency in production bundle

3. Package Import Optimization

experimental: {
  optimizePackageImports: [...featureFolders],
}

What this does:

Treats each feature like an external npm package, enabling:

Feature Benefit Impact
Tree-shaking Only bundles imported code 70% smaller bundles
Code splitting Better chunk distribution Faster page loads
Dead code elimination Removes unused exports Cleaner builds
Lazy loading Defers non-critical code Better performance

Real-world impact:

Scenario Without Optimization With Optimization
Import 1 component Bundles entire feature (500KB) Bundles only that component (50KB)
Import from 3 features 1.5MB total 150KB total
Build time Slower (analyzes everything) Faster (targeted analysis)
Hot reload Slower (rebuilds more) Faster (minimal rebuilds)

Example:

// Your code
import { LoginForm } from "@/features/auth";

// Without optimization
// ❌ Bundles: LoginForm, SignupForm, useAuth, authService,
//    validators, ResetPasswordForm, EmailVerification, etc.
// Total: 500KB

// With optimization
// βœ… Bundles: Only LoginForm + its direct dependencies
// Total: 45KB

Why This Approach Works

1. Zero Maintenance

// Just add a new feature
src/features/notifications/
  index.ts
  components/
  hooks/

// Next build automatically includes it in optimizePackageImports!

2. Performance by Default

  • Every feature gets tree-shaking
  • Prevents accidental bundle bloat
  • Keeps pages lean
  • No manual configuration needed

3. Predictable Builds

  • Filesystem read happens at build time
  • Same result every build
  • No runtime surprises
  • Works everywhere

Alternative: Explicit Configuration

If you prefer explicit configuration over dynamic discovery:

// Option 1: Explicit list (more predictable, more maintenance)
const nextConfig: NextConfig = {
  experimental: {
    optimizePackageImports: [
      "@/features/auth",
      "@/features/dashboard",
      "@/features/products",
      "@/features/settings",
      // ⚠️ Must add new features here manually
    ],
  },
};

Pros:

  • βœ… More explicit and predictable
  • βœ… No filesystem dependency
  • βœ… Clear what's optimized
  • βœ… Better for very large teams

Cons:

  • ❌ Must remember to update when adding features
  • ❌ More manual maintenance
  • ❌ Risk of forgetting to add new features

Recommendation: Stick with dynamic discovery for this architecture. It aligns perfectly with the "convention over configuration" philosophy.


πŸ“ TypeScript Configuration Explained

Understanding tsconfig.json

{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    "eslint.config.mjs",
    "independentModuleConfig.mjs"
  ],
  "exclude": ["node_modules"]
}

Configuration Breakdown

Essential Compiler Options

1. Target & Library

"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"]
  • target: Compiles to ES2017 (async/await support)
  • lib: Includes DOM APIs + latest JavaScript features
  • Why ES2017? Balance between modern features and broad browser support

2. Module Resolution

"module": "esnext",
"moduleResolution": "bundler"
  • module: Uses latest ESM (import/export)
  • moduleResolution: Optimized for bundlers (Next.js/Webpack)
  • Why bundler? Enables advanced optimizations like tree-shaking

3. Path Aliases - The Magic ✨

"paths": {
  "@/*": ["./src/*"]
}

What this does:

Transforms ugly relative imports into clean absolute imports:

// ❌ BEFORE: Ugly relative paths
import { LoginForm } from "../../../features/auth/components/LoginForm";
import { useProducts } from "../../../../features/products/hooks/useProducts";
import { Button } from "../../../components/ui/button";

// βœ… AFTER: Clean absolute paths
import { LoginForm } from "@/features/auth";
import { useProducts } from "@/features/products";
import { Button } from "@/components/ui/button";

Why this matters:

  1. Readability - Instantly know where imports come from
  2. Refactoring - Move files without breaking imports
  3. Consistency - Same import style everywhere
  4. IDE Support - Better autocomplete and navigation

How it works:

// You write:
import { something } from "@/features/auth";

// TypeScript resolves to:
import { something } from "./src/features/auth";

// In file: src/app/dashboard/products/page.tsx

4. Strict Type Checking

"strict": true

Enables all strict type checking options:

  • strictNullChecks - No implicit undefined
  • strictFunctionTypes - Safer function signatures
  • strictPropertyInitialization - Class properties must be initialized
  • noImplicitAny - Must explicitly type any
  • noImplicitThis - this must be typed

Why? Catches bugs at compile time, not runtime!


5. Next.js Integration

"jsx": "preserve",
"plugins": [{ "name": "next" }]
  • jsx: preserve - Keeps JSX for Next.js to transform
  • next plugin - Adds Next.js-specific type checking

6. Performance Options

"skipLibCheck": true,
"incremental": true,
"noEmit": true
  • skipLibCheck - Don't type-check node_modules (faster builds)
  • incremental - Cache results for faster rebuilds
  • noEmit - Don't output JS (Next.js handles that)

Include & Exclude

"include": [
  "next-env.d.ts",           // Next.js type definitions
  "**/*.ts",                 // All TypeScript files
  "**/*.tsx",                // All React files
  ".next/types/**/*.ts",     // Generated types
  "eslint.config.mjs",       // ESLint config
  "independentModuleConfig.mjs"  // Module config
],
"exclude": ["node_modules"]

Why include config files?

  • Provides TypeScript support in ESLint config
  • Type-safe module configuration
  • Better IDE experience

Path Aliases in Action

Example 1: Deep Nesting Made Simple

// File: src/app/dashboard/products/[id]/reviews/page.tsx

// ❌ WITHOUT path aliases
import { ProductReviews } from "../../../../../features/products/components/ProductReviews";
import { useAuth } from "../../../../../features/auth/hooks/useAuth";
import { Card } from "../../../../../components/ui/card";

// βœ… WITH path aliases
import { ProductReviews } from "@/features/products";
import { useAuth } from "@/features/auth";
import { Card } from "@/components/ui/card";

Example 2: Refactoring Resilience

// You move: src/app/products/page.tsx
//       to: src/app/store/products/page.tsx

// ❌ WITHOUT path aliases - ALL imports break!
import { ProductList } from "../../features/products"; // Now ❌ broken
import { Button } from "../../components/ui/button"; // Now ❌ broken

// βœ… WITH path aliases - NOTHING breaks!
import { ProductList } from "@/features/products"; // Still βœ… works
import { Button } from "@/components/ui/button"; // Still βœ… works

TypeScript + ESLint + Next.js Integration

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  You write code with @/ imports                β”‚
β”‚  import { X } from '@/features/auth'           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
             β”œβ”€β”€β†’ TypeScript checks types
             β”‚    βœ… Resolves @/ to src/
             β”‚    βœ… Validates imports exist
             β”‚    βœ… Type checks the code
             β”‚
             β”œβ”€β”€β†’ ESLint checks architecture
             β”‚    βœ… Validates import rules
             β”‚    βœ… Enforces layer boundaries
             β”‚    βœ… Prevents feature coupling
             β”‚
             └──→ Next.js bundles optimally
                  βœ… Tree-shakes unused code
                  βœ… Code splits features
                  βœ… Optimizes performance

🚦 ESLint Configuration Deep Dive

Understanding eslint.config.mjs

import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
import { independentModulesConfig } from "./independentModuleConfig.mjs";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
  baseDirectory: __dirname,
});

const eslintConfig = [
  {
    ignores: ["**/.next/**", "**/node_modules/**"],
  },
  ...compat.config({
    extends: [
      "next",
      "next/core-web-vitals",
      "plugin:@typescript-eslint/recommended-type-checked",
      "plugin:@typescript-eslint/strict-type-checked",
      "plugin:@typescript-eslint/stylistic-type-checked",
      "plugin:jsx-a11y/recommended",
      "prettier",
      "plugin:prettier/recommended",
    ],
    parser: "@typescript-eslint/parser",
    parserOptions: {
      projectService: true,
      tsconfigRootDir: __dirname,
    },
    root: true,
    plugins: ["@typescript-eslint", "eslint-plugin-project-structure"],
    rules: {
      "linebreak-style": ["error", "unix"],
      "@typescript-eslint/consistent-type-definitions": ["error", "type"],
      "@typescript-eslint/restrict-template-expressions": [
        "error",
        { allowNumber: true },
      ],
      "project-structure/independent-modules": [
        "error",
        independentModulesConfig,
      ],
    },
  }),
];

export default eslintConfig;

Configuration Breakdown

1. Modern ESLint Flat Config

import { FlatCompat } from "@eslint/eslintrc";

Why? ESLint is moving to a new "flat config" format. FlatCompat bridges old configs to new format.

Old vs New:

// ❌ OLD (.eslintrc.json)
{
  "extends": ["next"],
  "rules": {}
}

// βœ… NEW (eslint.config.mjs)
export default [
  { rules: {} }
];

2. File Ignoring

{
  ignores: ['**/.next/**', '**/node_modules/**'],
}

What's ignored:

  • .next/ - Build output (no need to lint)
  • node_modules/ - Third-party code
  • Uses glob patterns for flexibility

3. Extended Configurations

extends: [
  'next',                                           // Next.js best practices
  'next/core-web-vitals',                          // Performance rules
  'plugin:@typescript-eslint/recommended-type-checked',   // Type safety
  'plugin:@typescript-eslint/strict-type-checked',        // Strict types
  'plugin:@typescript-eslint/stylistic-type-checked',     // Code style
  'plugin:jsx-a11y/recommended',                   // Accessibility
  'prettier',                                      // Prettier integration
  'plugin:prettier/recommended',                   // Prettier as ESLint rules
],

Let's break down each configuration:

Config Purpose What It Does
next Next.js rules React Hooks, Link usage, Image optimization
next/core-web-vitals Performance CLS, LCP, FID optimizations
@typescript-eslint/recommended-type-checked Type safety Ensures type-safe code
@typescript-eslint/strict-type-checked Strict types No implicit any, strict null checks
@typescript-eslint/stylistic-type-checked Code style Consistent TypeScript patterns
jsx-a11y/recommended Accessibility ARIA labels, keyboard nav, semantic HTML
prettier Formatting Code formatting rules

Why so many? Each layer adds a specific type of protection:

Next.js rules       β†’ Framework best practices
TypeScript rules    β†’ Type safety & consistency
Accessibility rules β†’ Inclusive user experience
Prettier rules      β†’ Consistent formatting

4. TypeScript Parser Configuration

parser: '@typescript-eslint/parser',
parserOptions: {
  projectService: true,
  tsconfigRootDir: __dirname,
}

What this does:

  • parser: Uses TypeScript-aware parser for better analysis
  • projectService: Automatically finds and uses tsconfig.json
  • tsconfigRootDir: Sets the root for TypeScript configuration

Why projectService? Enables type-aware linting:

// Without type-aware linting
const x = await somePromise; // βœ… Looks fine

// With type-aware linting
const x = await somePromise; // ❌ Error: somePromise is not a Promise!

5. Custom Rules

rules: {
  'linebreak-style': ['error', 'unix'],
  '@typescript-eslint/consistent-type-definitions': ['error', 'type'],
  '@typescript-eslint/restrict-template-expressions': [
    'error',
    { allowNumber: true },
  ],
  'project-structure/independent-modules': ['error', independentModulesConfig],
}

Rule Breakdown:

a) Linebreak Style

'linebreak-style': ['error', 'unix']
  • Enforces Unix line endings (LF) not Windows (CRLF)
  • Prevents git diff noise
  • Consistent across all platforms

b) Type Definitions

'@typescript-eslint/consistent-type-definitions': ['error', 'type']
// ❌ NOT ALLOWED: interfaces
interface User {
  name: string;
}

// βœ… REQUIRED: type aliases
type User = {
  name: string;
};

Why prefer types?

  • More flexible (can use unions, intersections)
  • Consistent with our codebase style
  • Better for composition

c) Template Expressions

'@typescript-eslint/restrict-template-expressions': ['error', { allowNumber: true }]
const age = 25;
const name = "Alice";

// βœ… ALLOWED: Numbers in templates
const msg1 = `Age: ${age}`;

// βœ… ALLOWED: Strings in templates
const msg2 = `Name: ${name}`;

// ❌ NOT ALLOWED: Objects in templates
const obj = { x: 1 };
const msg3 = `Object: ${obj}`; // Error!

Why? Prevents common bugs like [object Object] in strings.

d) Independent Modules (THE KEY RULE)

'project-structure/independent-modules': ['error', independentModulesConfig]

This is the heart of our architecture enforcement! Let's dive deep into this...


πŸ—οΈ Independent Module Configuration

Understanding independentModuleConfig.mjs

// @ts-check

import { createIndependentModules } from "eslint-plugin-project-structure";

export const independentModulesConfig = createIndependentModules({
  modules: [
    {
      name: "App folder",
      pattern: "src/app/**",
      allowImportsFrom: [
        "src/app/global.css",
        "{sharedImports}",
        "{uiImports}",
        "src/features/*/index.(ts|tsx)",
      ],
      errorMessage:
        "πŸ”₯ App folder can only import from {sharedImports},{uiImports} and src/features/*/index.ts files πŸ”₯",
    },
    {
      name: "Features",
      pattern: "src/features/**",
      allowImportsFrom: ["{family_3}/**", "{sharedImports}", "{uiImports}"],
      errorMessage:
        "πŸ”₯ A feature may only import items from shared folders and its own family. Importing items from another feature is prohibited. πŸ”₯",
    },
    {
      name: "Shared",
      pattern: [
        "src/components/shared/**",
        "src/lib/*/**",
        "src/config/**",
        "src/types/**",
        "src/store/**",
        "src/utils/**",
      ],
      allowImportsFrom: ["{sharedImports}", "{uiImports}"],
      errorMessage:
        "πŸ”₯ Shared folders are not allowed to import items from the `features` folder. πŸ”₯",
    },
    {
      name: "UI",
      pattern: ["src/components/ui/*", "src/styles/**", "src/lib/*"],
      allowImportsFrom: ["{uiImports}"],
    },
    {
      name: "Unknown files",
      pattern: [["src/**", "!src/*"]],
      allowImportsFrom: [],
      allowExternalImports: false,
      errorMessage:
        "πŸ”₯ This file is not specified as an independent module in `independentModules.jsonc`. πŸ”₯",
    },
  ],
  reusableImportPatterns: {
    sharedImports: [
      "src/components/shared/**",
      "src/lib/*/**",
      "src/config/**",
      "src/types/**",
      "src/store/**",
      "src/utils/**",
    ],
    uiImports: ["src/components/ui/*", "src/styles/**", "src/lib/*"],
    neverImports: ["src/*"],
  },
});

Configuration Deep Dive

Module 1: App Folder

{
  name: 'App folder',
  pattern: 'src/app/**',
  allowImportsFrom: [
    'src/app/global.css',
    '{sharedImports}',
    '{uiImports}',
    'src/features/*/index.(ts|tsx)',
  ],
}

What this means:

// βœ… ALLOWED: Import global styles
import "@/app/global.css";

// βœ… ALLOWED: Import from shared folders
import { formatDate } from "@/utils/date";
import { API_URL } from "@/config/api";
import { useAppStore } from "@/store/app";
import { Container } from "@/components/shared/Container";

// βœ… ALLOWED: Import from UI layer
import { Button } from "@/components/ui/button";
import "@/styles/globals.css";

// βœ… ALLOWED: Import feature public API (index files ONLY)
import { LoginForm } from "@/features/auth";
import { ProductList } from "@/features/products";

// ❌ FORBIDDEN: Import feature internals
import { LoginForm } from "@/features/auth/components/LoginForm";
import { validateEmail } from "@/features/auth/utils/validation";

Pattern Explanation:

  • src/app/** - Matches all files in app folder
  • src/features/*/index.(ts|tsx) - Matches ONLY index files in ANY feature
    • src/features/auth/index.ts βœ…
    • src/features/auth/index.tsx βœ…
    • src/features/auth/components/LoginForm.tsx ❌

Module 2: Features

{
  name: 'Features',
  pattern: 'src/features/**',
  allowImportsFrom: ['{family_3}/**', '{sharedImports}', '{uiImports}'],
}

What this means:

// File: src/features/products/hooks/useProducts.ts

// βœ… ALLOWED: Import from own feature (family)
import { Product } from "../types";
import { fetchProducts } from "../api/productService";
import { ProductCard } from "../components/ProductCard";

// βœ… ALLOWED: Import from shared folders
import { useDebounce } from "@/utils/hooks";
import { API_URL } from "@/config/api";
import { useAppStore } from "@/store/app";

// βœ… ALLOWED: Import from UI layer
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";

// ❌ FORBIDDEN: Import from other features
import { useAuth } from "@/features/auth";
import { DashboardHeader } from "@/features/dashboard";

Pattern Explanation:

  • {family_3}/** - Special pattern meaning "same feature folder"
    • family_3 = 3 levels deep = src/features/products
    • For file src/features/products/hooks/useProducts.ts:
      • Can import from src/features/products/** βœ…
      • Cannot import from src/features/auth/** ❌

How family works:

src/features/products/hooks/useProducts.ts
β”‚         β”‚         β”‚
Level 1   Level 2   Level 3 (family!)

Family = src/features/products/
Can import anything from: src/features/products/**

Module 3: Shared Folders

{
  name: 'Shared',
  pattern: [
    'src/components/shared/**',
    'src/lib/*/**',
    'src/config/**',
    'src/types/**',
    'src/store/**',
    'src/utils/**',
  ],
  allowImportsFrom: ['{sharedImports}', '{uiImports}'],
}

What this means:

// File: src/components/shared/DataTable.tsx

// βœ… ALLOWED: Import from other shared folders
import { formatDate } from "@/utils/date";
import { API_URL } from "@/config/api";
import { useAppStore } from "@/store/app";

// βœ… ALLOWED: Import from UI layer
import { Table } from "@/components/ui/table";
import { Button } from "@/components/ui/button";

// ❌ FORBIDDEN: Import from features
import { useProducts } from "@/features/products";
import { useAuth } from "@/features/auth";

Pattern Explanation:

  • Multiple patterns = ALL these folders follow the same rule
  • src/lib/*/** matches src/lib/prisma/client.ts but not src/lib/utils.ts
    • Why? lib/utils.ts is UI layer, lib/*/ are shared integrations

Module 4: UI Layer

{
  name: 'UI',
  pattern: ['src/components/ui/*', 'src/styles/**', 'src/lib/*'],
  allowImportsFrom: ['{uiImports}'],
}

What this means:

// File: src/components/ui/button.tsx

// βœ… ALLOWED: Import from other UI components
import { cn } from "@/lib/utils";
import "./button.css";
import { Icon } from "@/components/ui/icon";

// ❌ FORBIDDEN: Import from shared layer
import { formatDate } from "@/utils/date";

// ❌ FORBIDDEN: Import from features
import { useAuth } from "@/features/auth";

Why src/lib/* is UI layer?

src/lib/utils.ts        β†’ UI layer (utilities)
src/lib/prisma/client.ts β†’ Shared layer (integration)

Pattern src/lib/* matches only files directly in lib/, not subfolders.


Module 5: Unknown Files Guard

{
  name: 'Unknown files',
  pattern: [['src/**', '!src/*']],
  allowImportsFrom: [],
  allowExternalImports: false,
}

What this does:

Catches any file NOT matched by previous rules!

// File: src/random/SomeFile.ts (not in any defined folder)

// ❌ ERROR: File not in architecture
// Must be in: app/, features/, components/, lib/, etc.

Pattern Explanation:

  • src/** - Match ALL files in src
  • !src/* - EXCEPT files directly in src root
  • Result: Catches files in undefined folders

Why allow files in src/*?

src/middleware.ts        βœ… OK (Next.js middleware)
src/instrumentation.ts   βœ… OK (Next.js instrumentation)
src/random/file.ts       ❌ ERROR (unknown folder)

Reusable Import Patterns

reusableImportPatterns: {
  sharedImports: [
    'src/components/shared/**',
    'src/lib/*/**',
    'src/config/**',
    'src/types/**',
    'src/store/**',
    'src/utils/**',
  ],
  uiImports: [
    'src/components/ui/*',
    'src/styles/**',
    'src/lib/*'
  ],
  neverImports: ['src/*'],
}

Why reusable patterns?

Instead of repeating the same list everywhere:

// ❌ WITHOUT reusable patterns - repetitive
{
  name: 'Features',
  allowImportsFrom: [
    'src/components/shared/**',
    'src/lib/*/**',
    'src/config/**',
    'src/types/**',
    'src/store/**',
    'src/utils/**',
    'src/components/ui/*',
    'src/styles/**',
    'src/lib/*',
  ]
}

// βœ… WITH reusable patterns - DRY
{
  name: 'Features',
  allowImportsFrom: ['{sharedImports}', '{uiImports}']
}

Benefits:

  • βœ… Update once, applies everywhere
  • βœ… Less duplication
  • βœ… Clearer intent

How the Enforcement Works

Real-time checking:

// File: src/features/products/hooks/useProducts.ts

// You type this:
import { useAuth } from "@/features/auth";

// ESLint immediately shows:
// πŸ”₯ A feature may only import items from shared folders
//    and its own family. Importing items from another
//    feature is prohibited. πŸ”₯

IDE Integration:

VS Code with ESLint extension:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ import { useAuth } from '@/features/auth'β”‚
β”‚        ~~~~~~~~ ❌                       β”‚
β”‚                                         β”‚
β”‚ πŸ”₯ A feature may only import items     β”‚
β”‚    from shared folders and its own      β”‚
β”‚    family. Importing from another       β”‚
β”‚    feature is prohibited. πŸ”₯            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Pre-commit hooks (recommended):

# .husky/pre-commit
npm run lint

# Prevents commits with architecture violations!

Pattern Matching Examples

Understanding glob patterns:

Pattern Matches Doesn't Match
src/app/** src/app/page.tsx
src/app/dashboard/page.tsx
src/features/auth/index.ts
src/features/*/index.ts src/features/auth/index.ts
src/features/products/index.ts
src/features/auth/components/Login.tsx
src/features/auth/index.test.ts
src/lib/* src/lib/utils.ts src/lib/prisma/client.ts
src/lib/*/** src/lib/prisma/client.ts
src/lib/stripe/checkout.ts
src/lib/utils.ts
{family_3}/** (Same folder at level 3) (Different folders)

Family pattern in action:

// File location: src/features/auth/hooks/useAuth.ts
//                β”‚   β”‚        β”‚    β”‚
//                1   2        3    4 (levels deep)

// family_3 = src/features/auth/

// βœ… Can import (same family):
import { Login } from "../components/Login"; // src/features/auth/components/Login
import { User } from "../types"; // src/features/auth/types
import { authService } from "../api/authService"; // src/features/auth/api/authService

// ❌ Cannot import (different family):
import { Product } from "@/features/products/types"; // src/features/products/types

πŸ“š Workflow Guide

Adding a New Feature

Step 1: Create the feature structure

mkdir -p src/features/notifications
cd src/features/notifications
touch index.ts
mkdir components hooks api utils types

Step 2: Build your feature

// components/NotificationBell.tsx
import { Bell } from "@/components/ui/icons";
import { useNotifications } from "../hooks/useNotifications";

export function NotificationBell() {
  const { unreadCount } = useNotifications();

  return (
    <button className="relative">
      <Bell />
      {unreadCount > 0 && (
        <span className="absolute -top-1 -right-1 bg-red-500 text-white rounded-full w-5 h-5 text-xs">
          {unreadCount}
        </span>
      )}
    </button>
  );
}

// hooks/useNotifications.ts
import { useEffect, useState } from "react";
import { fetchNotifications } from "../api/notificationService";
import type { Notification } from "../types";

export function useNotifications() {
  const [notifications, setNotifications] = useState<Notification[]>([]);
  const [unreadCount, setUnreadCount] = useState(0);

  useEffect(() => {
    fetchNotifications().then((data) => {
      setNotifications(data);
      setUnreadCount(data.filter((n) => !n.read).length);
    });
  }, []);

  return { notifications, unreadCount };
}

// api/notificationService.ts (INTERNAL - not exported)
export async function fetchNotifications() {
  const response = await fetch("/api/notifications");
  return response.json();
}

// types.ts
export type Notification = {
  id: string;
  title: string;
  message: string;
  read: boolean;
  createdAt: Date;
};

Step 3: Define the public API (CRITICAL)

// index.ts - The ONLY file app layer can import from
export { NotificationBell } from "./components/NotificationBell";
export { useNotifications } from "./hooks/useNotifications";
export type { Notification } from "./types";

// ⚠️ Keep internal utilities private!
// fetchNotifications from api/notificationService is NOT exported
// This keeps implementation details hidden

Step 4: Use in app layer

// app/dashboard/page.tsx
import { NotificationBell } from "@/features/notifications";

export default function Dashboard() {
  return (
    <header>
      <h1>Dashboard</h1>
      <NotificationBell />
    </header>
  );
}

Step 5: Verify everything works

# Check TypeScript
npm run type-check

# Check ESLint (architecture rules)
npm run lint

# Check bundle optimization
npm run build

# Your new feature is automatically included in optimizePackageImports! πŸŽ‰

Adding a Shared Component

When to create a shared component:

Create Shared When Keep in Feature When
Used by 2+ features Used by 1 feature only
Has cross-cutting concerns Feature-specific logic
Generic business logic Tightly coupled to feature domain

Example: Creating a shared DatePicker

// components/shared/DatePicker.tsx
import { Calendar } from "@/components/ui/calendar";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { formatDate } from "@/utils/date";
import { CalendarIcon } from "@/components/ui/icons";

type DatePickerProps = {
  value?: Date;
  onChange: (date: Date) => void;
  minDate?: Date;
  maxDate?: Date;
};

export function DatePicker({
  value,
  onChange,
  minDate,
  maxDate,
}: DatePickerProps) {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline">
          <CalendarIcon />
          {value ? formatDate(value) : "Pick a date"}
        </Button>
      </PopoverTrigger>
      <PopoverContent>
        <Calendar
          mode="single"
          selected={value}
          onSelect={onChange}
          disabled={(date) =>
            (minDate && date < minDate) || (maxDate && date > maxDate)
          }
        />
      </PopoverContent>
    </Popover>
  );
}

Now use it in multiple features:

// features/bookings/components/BookingForm.tsx
import { DatePicker } from "@/components/shared/DatePicker";

export function BookingForm() {
  const [startDate, setStartDate] = useState<Date>();

  return (
    <form>
      <DatePicker value={startDate} onChange={setStartDate} />
    </form>
  );
}

// features/reports/components/ReportFilters.tsx
import { DatePicker } from "@/components/shared/DatePicker";

export function ReportFilters() {
  const [fromDate, setFromDate] = useState<Date>();

  return (
    <div>
      <DatePicker value={fromDate} onChange={setFromDate} />
    </div>
  );
}

Refactoring: Moving Code Between Layers

Scenario: Your feature component is now used everywhere. Time to promote it!

Step 1: Identify the component

// features/products/components/PriceDisplay.tsx
import { formatCurrency } from "@/utils/format";

type PriceDisplayProps = {
  amount: number;
  currency?: string;
};

export function PriceDisplay({ amount, currency = "USD" }: PriceDisplayProps) {
  return (
    <span className="font-semibold text-lg">
      {formatCurrency(amount, currency)}
    </span>
  );
}

Step 2: Check dependencies

# Find all imports in the file
grep -n "^import" features/products/components/PriceDisplay.tsx

# Result:
# import { formatCurrency } from '@/utils/format';

# βœ… Only imports from shared layer (utils)
# βœ… Safe to move to shared!

If it imports from features:

// ❌ HAS feature dependency - DON'T move yet
import { useProducts } from "../hooks/useProducts";

// Fix: Remove feature dependency first

Step 3: Move to shared

git mv src/features/products/components/PriceDisplay.tsx \
       src/components/shared/PriceDisplay.tsx

Step 4: Update all imports

# Find all usages
grep -r "PriceDisplay" src/

# Update imports across codebase
// features/products/index.ts
// ❌ Remove: export { PriceDisplay } from './components/PriceDisplay';

// features/products/components/ProductCard.tsx
// ❌ OLD: import { PriceDisplay } from './PriceDisplay';
// βœ… NEW: import { PriceDisplay } from '@/components/shared/PriceDisplay';

// features/orders/components/OrderItem.tsx
// ❌ OLD: import { PriceDisplay } from '@/features/products';
// βœ… NEW: import { PriceDisplay } from '@/components/shared/PriceDisplay';

Step 5: Verify no violations

npm run lint
npm run type-check
npm run build

Feature Communication Patterns

❌ WRONG: Direct feature imports

// features/cart/hooks/useCart.ts
import { useProducts } from "@/features/products"; // ❌ ESLint error!

export function useCart() {
  const products = useProducts(); // Direct dependency
  // ...
}

βœ… RIGHT: Dependency injection from app

// features/cart/hooks/useCart.ts
import type { Product } from "@/types/product";

export function useCart(products: Product[]) {
  // Cart logic uses products passed as parameter
  // No direct dependency!

  const addToCart = (productId: string) => {
    const product = products.find((p) => p.id === productId);
    // ...
  };

  return { addToCart };
}

// app/checkout/page.tsx
import { useProducts } from "@/features/products";
import { useCart } from "@/features/cart";

export default function CheckoutPage() {
  const products = useProducts();
  const cart = useCart(products); // App layer connects them

  return <CartView cart={cart} />;
}

βœ… Dos and Don'ts

The Golden Rules

βœ… DO

Rule Example Why
Use index.ts as public API export { LoginForm } from './components' Clear boundaries, encapsulation
Keep features independent Pass data down from app layer Scalability, testability
Use shared for common logic shared/hooks/useDebounce.ts DRY principle, reusability
Keep UI components pure No useAuth() in Button Reusability, design system
Name features by domain features/authentication/ not features/stuff/ Clarity, maintainability
Export types from features export type { User } Type safety across boundaries
Use absolute imports @/features/auth not ../../auth Readability, refactor-safety
One feature = one domain auth/ not auth-and-profile/ Single responsibility

❌ DON'T

Mistake Example Why It's Bad
Import feature internals import X from '@/features/auth/utils/hash' Breaks encapsulation, coupling
Cross-feature imports features/products imports from features/auth Creates dependency web
Business logic in UI Button component with API calls Violates separation of concerns
Export everything export * from './components' Bundle bloat, unclear API
Generic feature names features/misc/ or features/common/ Becomes dumping ground
Shared importing features shared/Header imports features/auth Circular dependencies
Deep nesting features/auth/components/forms/login/fields/ Hard to navigate, import hell
Mixing concerns Auth logic in products feature Unclear boundaries

Code Smell Checklist

🚨 Your code might need refactoring if:

  • You're importing from ../../../features/
  • Your UI component needs business logic
  • Your feature imports another feature
  • Your index.ts has 50+ exports
  • You're bypassing index.ts to import internals
  • Your shared component is only used by one feature
  • Your build size keeps growing mysteriously
  • You have circular dependency warnings
  • ESLint keeps complaining about imports
  • TypeScript can't resolve imports

Refactoring Decision Tree

Found duplicate code?
β”‚
β”œβ”€ Used by 2+ features?
β”‚  β”‚
β”‚  β”œβ”€ YES β†’ Move to shared/
β”‚  β”‚        └─ Has business logic?
β”‚  β”‚           β”œβ”€ YES β†’ components/shared/
β”‚  β”‚           └─ NO β†’ components/ui/
β”‚  β”‚
β”‚  └─ NO β†’ Keep in feature
β”‚
└─ Feature depends on another feature?
   β”‚
   β”œβ”€ Can logic move to shared?
   β”‚  β”œβ”€ YES β†’ Move to shared/
   β”‚  └─ NO β†’ Pass data from app layer
   β”‚
   └─ Should features be merged?
       β”œβ”€ YES β†’ Combine into one feature
       └─ NO β†’ Keep separate, use app layer

🎯 Real-World Examples

Example 1: Authentication Flow

❌ WRONG: Tightly coupled

// features/products/components/ProductList.tsx
import { useAuth } from "@/features/auth"; // ❌ Cross-feature import

export function ProductList() {
  const { user } = useAuth();

  if (!user) return <LoginForm />; // ❌ Products depends on auth UI

  return <div>Products for {user.name}</div>;
}

Problems:

  • Products feature depends on auth feature
  • Can't test products without auth
  • Can't reuse products in public pages

βœ… RIGHT: Composed at app layer

// features/products/components/ProductList.tsx
type ProductListProps = {
  userName: string;
};

export function ProductList({ userName }: ProductListProps) {
  return <div>Products for {userName}</div>;
}

// app/products/page.tsx
import { redirect } from "next/navigation";
import { getServerSession } from "@/features/auth";
import { ProductList } from "@/features/products";

export default async function ProductsPage() {
  const session = await getServerSession();

  if (!session) redirect("/login");

  return <ProductList userName={session.user.name} />;
}

Benefits:

  • βœ… Products feature is independent
  • βœ… Easy to test in isolation
  • βœ… Can pass different data sources
  • βœ… App layer controls the flow

Example 2: Shared Data Display

Scenario: Multiple features need to display user data.

❌ WRONG: Duplicated in each feature

// features/products/components/ProductAuthor.tsx
import { useQuery } from "@tanstack/react-query";

export function ProductAuthor({ userId }: { userId: string }) {
  const { data: user } = useQuery({
    queryKey: ["user", userId],
    queryFn: () => fetch(`/api/users/${userId}`).then((r) => r.json()),
  });

  if (!user) return <div>Loading...</div>;

  return (
    <div>
      <img src={user.avatar} alt={user.name} />
      <span>{user.name}</span>
    </div>
  );
}

// features/comments/components/CommentAuthor.tsx
// ❌ Same logic duplicated!
export function CommentAuthor({ userId }: { userId: string }) {
  const { data: user } = useQuery({
    queryKey: ["user", userId],
    queryFn: () => fetch(`/api/users/${userId}`).then((r) => r.json()),
  });

  if (!user) return <div>Loading...</div>;

  return (
    <div>
      <img src={user.avatar} alt={user.name} />
      <span>{user.name}</span>
    </div>
  );
}

Problems:

  • Code duplication
  • Inconsistent UI
  • Hard to maintain
  • Both features make separate API calls

βœ… RIGHT: Shared component + hook

// store/users.ts
import { create } from "zustand";

type User = {
  id: string;
  name: string;
  avatar: string;
};

type UserStore = {
  users: Record<string, User>;
  fetchUser: (id: string) => Promise<User>;
};

export const useUserStore = create<UserStore>((set, get) => ({
  users: {},
  fetchUser: async (id) => {
    const existing = get().users[id];
    if (existing) return existing;

    const user = await fetch(`/api/users/${id}`).then((r) => r.json());
    set((state) => ({ users: { ...state.users, [id]: user } }));
    return user;
  },
}));

// components/shared/UserDisplay.tsx
import { useEffect, useState } from "react";
import { Avatar } from "@/components/ui/avatar";
import { useUserStore } from "@/store/users";

type UserDisplayProps = {
  userId: string;
  showEmail?: boolean;
};

export function UserDisplay({ userId, showEmail = false }: UserDisplayProps) {
  const { users, fetchUser } = useUserStore();
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!users[userId]) {
      setLoading(true);
      fetchUser(userId).finally(() => setLoading(false));
    }
  }, [userId, fetchUser, users]);

  const user = users[userId];

  if (loading) return <div className="animate-pulse">Loading...</div>;
  if (!user) return null;

  return (
    <div className="flex items-center gap-2">
      <Avatar src={user.avatar} alt={user.name} />
      <div>
        <p className="font-medium">{user.name}</p>
        {showEmail && <p className="text-sm text-muted">{user.email}</p>}
      </div>
    </div>
  );
}

// features/products/components/ProductAuthor.tsx
import { UserDisplay } from "@/components/shared/UserDisplay";

export function ProductAuthor({ userId }: { userId: string }) {
  return <UserDisplay userId={userId} />;
}

// features/comments/components/CommentAuthor.tsx
import { UserDisplay } from "@/components/shared/UserDisplay";

export function CommentAuthor({ userId }: { userId: string }) {
  return <UserDisplay userId={userId} showEmail />;
}

Benefits:

  • βœ… Single source of truth
  • βœ… Consistent UI across features
  • βœ… Cached user data (no duplicate API calls)
  • βœ… Easy to extend (add more props)
  • βœ… Maintainable in one place

Example 3: Feature Communication via Context

Scenario: Shopping cart needs product data.

❌ WRONG: Direct feature imports

// features/cart/hooks/useCart.ts
import { useProducts } from "@/features/products"; // ❌

export function useCart() {
  const products = useProducts(); // Direct dependency

  const addToCart = (productId: string) => {
    const product = products.find((p) => p.id === productId);
    // ...
  };

  return { addToCart };
}

βœ… RIGHT: App-level data provider

// app/providers.tsx
"use client";

import { createContext, useContext, ReactNode } from "react";
import { useProducts } from "@/features/products";
import type { Product } from "@/types/product";

const ProductContext = createContext<Product[]>([]);

export function useProductContext() {
  return useContext(ProductContext);
}

export function ProductProvider({ children }: { children: ReactNode }) {
  const products = useProducts();

  return (
    <ProductContext.Provider value={products}>
      {children}
    </ProductContext.Provider>
  );
}

// features/cart/hooks/useCart.ts
import { useProductContext } from "@/app/providers";

export function useCart() {
  const products = useProductContext(); // From app layer

  const addToCart = (productId: string) => {
    const product = products.find((p) => p.id === productId);
    // ...
  };

  return { addToCart };
}

// app/layout.tsx
import { ProductProvider } from "./providers";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ProductProvider>{children}</ProductProvider>
      </body>
    </html>
  );
}

Benefits:

  • βœ… Features remain independent
  • βœ… App layer controls data flow
  • βœ… Easy to test features in isolation
  • βœ… Can swap data sources easily

Example 4: Form with Validation

❌ WRONG: Validation scattered everywhere

// features/auth/components/SignupForm.tsx
export function SignupForm() {
  const handleSubmit = (data: FormData) => {
    // ❌ Validation logic in component
    const email = data.get("email") as string;
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      alert("Invalid email");
      return;
    }
    // ...
  };
}

// features/profile/components/ProfileForm.tsx
export function ProfileForm() {
  const handleSubmit = (data: FormData) => {
    // ❌ Same validation duplicated
    const email = data.get("email") as string;
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      alert("Invalid email");
      return;
    }
    // ...
  };
}

βœ… RIGHT: Centralized validation

// utils/validation.ts
import { z } from "zod";

export const emailSchema = z.string().email("Invalid email address");
export const passwordSchema = z
  .string()
  .min(8, "Password must be at least 8 characters");

export const signupSchema = z
  .object({
    email: emailSchema,
    password: passwordSchema,
    confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "Passwords don't match",
    path: ["confirmPassword"],
  });

export const profileSchema = z.object({
  email: emailSchema,
  name: z.string().min(2, "Name must be at least 2 characters"),
  bio: z.string().max(500, "Bio must be less than 500 characters").optional(),
});

// features/auth/components/SignupForm.tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { signupSchema } from "@/utils/validation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";

export function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(signupSchema),
  });

  const onSubmit = (data) => {
    // Data is already validated!
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Input {...register("email")} />
      {errors.email && <p className="text-red-500">{errors.email.message}</p>}

      <Input type="password" {...register("password")} />
      {errors.password && (
        <p className="text-red-500">{errors.password.message}</p>
      )}

      <Input type="password" {...register("confirmPassword")} />
      {errors.confirmPassword && (
        <p className="text-red-500">{errors.confirmPassword.message}</p>
      )}

      <Button type="submit">Sign Up</Button>
    </form>
  );
}

// features/profile/components/ProfileForm.tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { profileSchema } from "@/utils/validation";

export function ProfileForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(profileSchema),
  });

  const onSubmit = (data) => {
    // Data is already validated!
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Input {...register("email")} />
      {errors.email && <p className="text-red-500">{errors.email.message}</p>}

      <Input {...register("name")} />
      {errors.name && <p className="text-red-500">{errors.name.message}</p>}

      <Input {...register("bio")} />
      {errors.bio && <p className="text-red-500">{errors.bio.message}</p>}

      <Button type="submit">Update Profile</Button>
    </form>
  );
}

Benefits:

  • βœ… Single source of truth for validation
  • βœ… Reusable schemas
  • βœ… Type-safe forms
  • βœ… Consistent error messages
  • βœ… Easy to test validation logic

Example 5: API Client Pattern

❌ WRONG: Fetch scattered everywhere

// features/products/hooks/useProducts.ts
export function useProducts() {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    fetch("https://api.example.com/products")
      .then((r) => r.json())
      .then(setProducts);
  }, []);

  return products;
}

// features/orders/hooks/useOrders.ts
export function useOrders() {
  const [orders, setOrders] = useState([]);

  useEffect(() => {
    fetch("https://api.example.com/orders")
      .then((r) => r.json())
      .then(setOrders);
  }, []);

  return orders;
}

βœ… RIGHT: Centralized API client

// lib/api/client.ts
type ApiConfig = {
  baseURL: string;
  headers?: HeadersInit;
};

class ApiClient {
  private baseURL: string;
  private headers: HeadersInit;

  constructor(config: ApiConfig) {
    this.baseURL = config.baseURL;
    this.headers = config.headers || {};
  }

  async get<T>(endpoint: string): Promise<T> {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      method: "GET",
      headers: this.headers,
    });

    if (!response.ok) {
      throw new Error(`API Error: ${response.statusText}`);
    }

    return response.json();
  }

  async post<T>(endpoint: string, data: unknown): Promise<T> {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      method: "POST",
      headers: {
        ...this.headers,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(data),
    });

    if (!response.ok) {
      throw new Error(`API Error: ${response.statusText}`);
    }

    return response.json();
  }
}

export const apiClient = new ApiClient({
  baseURL: process.env.NEXT_PUBLIC_API_URL || "https://api.example.com",
  headers: {
    "X-API-Key": process.env.NEXT_PUBLIC_API_KEY || "",
  },
});

// features/products/api/productService.ts
import { apiClient } from "@/lib/api/client";
import type { Product } from "../types";

export async function fetchProducts(): Promise<Product[]> {
  return apiClient.get<Product[]>("/products");
}

export async function fetchProductById(id: string): Promise<Product> {
  return apiClient.get<Product>(`/products/${id}`);
}

export async function createProduct(
  product: Omit<Product, "id">
): Promise<Product> {
  return apiClient.post<Product>("/products", product);
}

// features/products/hooks/useProducts.ts
import { useQuery } from "@tanstack/react-query";
import { fetchProducts } from "../api/productService";

export function useProducts() {
  return useQuery({
    queryKey: ["products"],
    queryFn: fetchProducts,
  });
}

Benefits:

  • βœ… Centralized error handling
  • βœ… Consistent headers (auth, API keys)
  • βœ… Easy to add interceptors
  • βœ… Type-safe API calls
  • βœ… Easy to mock for testing
  • βœ… Can swap implementations (REST β†’ GraphQL)

πŸ”§ Troubleshooting

Common Issues & Solutions

Issue 1: "Cannot import from another feature"

Error:

πŸ”₯ A feature may only import items from shared folders and its own family.
Importing items from another feature is prohibited. πŸ”₯

  src/features/products/hooks/useProducts.ts
  > import { useAuth } from '@/features/auth';

Solutions:

Option 1: Move common logic to shared

// ❌ BEFORE
// features/products/hooks/useProducts.ts
import { useAuth } from "@/features/auth";

export function useProducts() {
  const { user } = useAuth();
  // ...
}

// βœ… AFTER
// features/products/hooks/useProducts.ts
export function useProducts(userId?: string) {
  // Accept userId as parameter
  // ...
}

// app/products/page.tsx
import { useAuth } from "@/features/auth";
import { useProducts } from "@/features/products";

export default function ProductsPage() {
  const { user } = useAuth();
  const products = useProducts(user?.id); // App layer connects them

  return <ProductList products={products} />;
}

Option 2: Use app-level provider

// app/providers.tsx
import { createContext, useContext } from "react";
import { useAuth } from "@/features/auth";

const AuthContext = createContext(null);

export function useAuthContext() {
  return useContext(AuthContext);
}

export function AuthProvider({ children }) {
  const auth = useAuth();
  return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
}

// features/products/hooks/useProducts.ts
import { useAuthContext } from "@/app/providers";

export function useProducts() {
  const { user } = useAuthContext(); // From app layer
  // ...
}

Option 3: Consider merging features

If two features are tightly coupled, maybe they should be one feature!

// features/user-products/
//   β”œβ”€β”€ auth/
//   β”œβ”€β”€ products/
//   └── index.ts

Issue 2: Large bundle size despite optimization

Symptoms:

npm run build

Page                              Size     First Load JS
β”Œ β—‹ /products                    450 kB         525 kB  # 🚨 Too large!

Check 1: Are you exporting too much?

// ❌ BAD: features/products/index.ts
export * from "./components"; // Exports ALL 20 components
export * from "./hooks"; // Exports ALL hooks
export * from "./utils"; // Exports ALL utilities

// βœ… GOOD: features/products/index.ts
export { ProductList } from "./components/ProductList";
export { ProductCard } from "./components/ProductCard";
export { useProducts } from "./hooks/useProducts";

Check 2: Is optimizePackageImports configured?

# Verify your next.config.ts has:
grep -A 5 "optimizePackageImports" next.config.ts

# Should see:
# optimizePackageImports: ['@/features/auth', '@/features/products', ...]

Check 3: Run bundle analyzer

ANALYZE=true npm run build

This opens an interactive visualization. Look for:

  • πŸ” Duplicate packages
  • πŸ” Unexpectedly large modules
  • πŸ” Unused dependencies

Check 4: Are you importing the right way?

// ❌ BAD: Imports feature internals (bypasses optimization)
import { ProductCard } from "@/features/products/components/ProductCard";

// βœ… GOOD: Imports from public API (benefits from optimization)
import { ProductCard } from "@/features/products";

Issue 3: Circular dependency

Symptoms:

Warning: Circular dependency detected:
  src/components/shared/Header.tsx
  -> src/features/auth/index.ts
  -> src/components/shared/UserMenu.tsx
  -> src/components/shared/Header.tsx

Cause: Layers importing from higher layers

// ❌ CIRCULAR DEPENDENCY
// components/shared/Header.tsx
import { useAuth } from "@/features/auth"; // Shared β†’ Features

// features/auth/components/UserMenu.tsx
import { Header } from "@/components/shared/Header"; // Features β†’ Shared

Solution: Follow the layer hierarchy

UI β†’ Shared β†’ Features β†’ App
(Lower layers NEVER import from higher layers)

Fix:

// βœ… CORRECT
// components/shared/Header.tsx
type HeaderProps = {
  user?: { name: string; avatar: string };
  onLogout?: () => void;
};

export function Header({ user, onLogout }: HeaderProps) {
  // No feature imports!
  return (
    <header>{user && <UserMenu user={user} onLogout={onLogout} />}</header>
  );
}

// app/layout.tsx
import { Header } from "@/components/shared/Header";
import { useAuth } from "@/features/auth";

export default function RootLayout({ children }) {
  const { user, logout } = useAuth();

  return (
    <html>
      <body>
        <Header user={user} onLogout={logout} />
        {children}
      </body>
    </html>
  );
}

Issue 4: New feature not optimized

Symptoms:

# After adding features/notifications/
npm run build

# Bundle still large, optimization not working

Check 1: Feature structure

ls -la src/features/notifications/

# Should have:
# index.ts  ← REQUIRED!
# components/
# hooks/
# etc.

Check 2: Rebuild

# Clear Next.js cache
rm -rf .next

# Rebuild
npm run build

The config dynamically reads features at build time, so a clean rebuild should detect it.

Check 3: Manual verification

# Check next.config.ts is reading features
node -e "
  const { readdirSync } = require('fs');
  const path = require('path');
  const featuresDir = path.resolve(__dirname, 'src/features');
  const features = readdirSync(featuresDir);
  console.log('Features found:', features);
"

# Should output: Features found: [ 'auth', 'products', 'notifications', ... ]

Check 4: Verify in build output

npm run build -- --debug

# Look for lines like:
# Optimizing package imports: @/features/auth, @/features/products, @/features/notifications

Issue 5: TypeScript can't resolve imports

Symptoms:

import { LoginForm } from "@/features/auth";
//                        ^^^^^^^^^^^^^^^^^ Cannot find module

Check 1: tsconfig.json paths

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"] // ← Must be present
    }
  }
}

Check 2: Restart TypeScript server

In VS Code:

  1. Cmd/Ctrl + Shift + P
  2. "TypeScript: Restart TS Server"

Check 3: Verify file exists

ls src/features/auth/index.ts
# If missing, create it!

Check 4: Check for typos

// ❌ WRONG: Case sensitivity
import { LoginForm } from "@/Features/auth";

// βœ… CORRECT
import { LoginForm } from "@/features/auth";

Issue 6: ESLint not catching violations

Symptoms:

// This should error but doesn't:
import { useAuth } from "@/features/auth"; // In products feature

Check 1: ESLint running?

npm run lint

# If not set up:
npm run lint -- src/features/products/hooks/useProducts.ts

Check 2: Plugin installed?

npm list eslint-plugin-project-structure

# Should show version
# If missing:
npm install -D eslint-plugin-project-structure

Check 3: Config loaded?

# Verify eslint.config.mjs imports independentModuleConfig
grep "independentModulesConfig" eslint.config.mjs

# Should see: import { independentModulesConfig } from './independentModuleConfig.mjs';

Check 4: IDE integration

In VS Code, install ESLint extension:

code --install-extension dbaeumer.vscode-eslint

Restart VS Code after installation.


Performance Optimization Tips

1. Monitor Bundle Size

Add to package.json:

{
  "scripts": {
    "build": "next build",
    "build:analyze": "ANALYZE=true next build",
    "build:check": "npm run build && ls -lh .next/static/chunks/pages/*.js"
  }
}

Run regularly:

npm run build:analyze

2. Code Splitting Best Practices

// βœ… GOOD: Dynamic imports for heavy features
import dynamic from "next/dynamic";

const AdminDashboard = dynamic(() => import("@/features/admin"), {
  loading: () => <div>Loading admin panel...</div>,
});

export default function AdminPage() {
  return <AdminDashboard />;
}

3. Optimize Images

// βœ… Use Next.js Image component
import Image from "next/image";

<Image
  src="/product.jpg"
  alt="Product"
  width={500}
  height={500}
  loading="lazy"
/>;

4. Use Server Components

// app/products/page.tsx - Server Component by default
import { ProductList } from "@/features/products";

export default async function ProductsPage() {
  // Fetch data on server
  const products = await fetch("https://api.example.com/products").then((r) =>
    r.json()
  );

  return <ProductList products={products} />;
}

// features/products/components/ProductList.tsx - Client Component only when needed
("use client");

export function ProductList({ products }) {
  // Interactive features
  const [filter, setFilter] = useState("");

  return (
    <div>
      <input value={filter} onChange={(e) => setFilter(e.target.value)} />
      {products
        .filter((p) => p.name.includes(filter))
        .map((p) => (
          <ProductCard key={p.id} product={p} />
        ))}
    </div>
  );
}

πŸ“š Configuration Files Reference

Complete tsconfig.json

{
  "compilerOptions": {
    // Language & Runtime
    "target": "ES2017", // Compile to ES2017 (async/await support)
    "lib": ["dom", "dom.iterable", "esnext"], // Include DOM & latest JS features
    "jsx": "preserve", // Keep JSX for Next.js to transform

    // Module System
    "module": "esnext", // Use latest ESM
    "moduleResolution": "bundler", // Optimize for bundlers (Webpack/Next.js)
    "resolveJsonModule": true, // Allow importing JSON files
    "esModuleInterop": true, // Better CommonJS interop
    "isolatedModules": true, // Each file can be transpiled independently

    // Type Checking
    "strict": true, // Enable all strict type checks
    "skipLibCheck": true, // Don't type-check node_modules (faster)
    "allowJs": true, // Allow JavaScript files

    // Emit
    "noEmit": true, // Don't output JS (Next.js handles it)
    "incremental": true, // Cache for faster rebuilds

    // Path Mapping
    "paths": {
      "@/*": ["./src/*"] // Enable @/ imports
    },

    // Next.js Integration
    "plugins": [
      {
        "name": "next" // Next.js TypeScript plugin
      }
    ]
  },
  "include": [
    "next-env.d.ts", // Next.js types
    "**/*.ts", // All TypeScript files
    "**/*.tsx", // All React files
    ".next/types/**/*.ts", // Generated types
    "eslint.config.mjs", // ESLint config
    "independentModuleConfig.mjs" // Module config
  ],
  "exclude": [
    "node_modules" // Don't include dependencies
  ]
}

Complete eslint.config.mjs

import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
import { independentModulesConfig } from "./independentModuleConfig.mjs";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
  baseDirectory: __dirname,
});

const eslintConfig = [
  {
    ignores: ["**/.next/**", "**/node_modules/**"],
  },
  ...compat.config({
    extends: [
      "next", // Next.js best practices
      "next/core-web-vitals", // Performance rules
      "plugin:@typescript-eslint/recommended-type-checked", // Type safety
      "plugin:@typescript-eslint/strict-type-checked", // Strict types
      "plugin:@typescript-eslint/stylistic-type-checked", // Code style
      "plugin:jsx-a11y/recommended", // Accessibility
      "prettier", // Prettier integration
      "plugin:prettier/recommended", // Prettier as ESLint rules
    ],
    parser: "@typescript-eslint/parser",
    parserOptions: {
      projectService: true, // Auto-find tsconfig
      tsconfigRootDir: __dirname, // TypeScript root
    },
    root: true,
    plugins: ["@typescript-eslint", "eslint-plugin-project-structure"],
    rules: {
      // Code Style
      "linebreak-style": ["error", "unix"],
      "@typescript-eslint/consistent-type-definitions": ["error", "type"],
      "@typescript-eslint/restrict-template-expressions": [
        "error",
        { allowNumber: true },
      ],

      // Architecture Enforcement (THE KEY RULE)
      "project-structure/independent-modules": [
        "error",
        independentModulesConfig,
      ],
    },
  }),
];

export default eslintConfig;

Complete independentModuleConfig.mjs

// @ts-check

import { createIndependentModules } from "eslint-plugin-project-structure";

export const independentModulesConfig = createIndependentModules({
  modules: [
    // Layer 4: App (Most Permissive)
    {
      name: "App folder",
      pattern: "src/app/**",
      allowImportsFrom: [
        "src/app/global.css", // Global styles
        "{sharedImports}", // Shared layer
        "{uiImports}", // UI layer
        "src/features/*/index.(ts|tsx)", // Feature public APIs only
      ],
      errorMessage:
        "πŸ”₯ App folder can only import from {sharedImports},{uiImports} and src/features/*/index.ts files πŸ”₯",
    },

    // Layer 3: Features
    {
      name: "Features",
      pattern: "src/features/**",
      allowImportsFrom: [
        "{family_3}/**", // Own feature files
        "{sharedImports}", // Shared layer
        "{uiImports}", // UI layer
      ],
      errorMessage:
        "πŸ”₯ A feature may only import items from shared folders and its own family. Importing items from another feature is prohibited. πŸ”₯",
    },

    // Layer 2: Shared
    {
      name: "Shared",
      pattern: [
        "src/components/shared/**",
        "src/lib/*/**",
        "src/config/**",
        "src/types/**",
        "src/store/**",
        "src/utils/**",
      ],
      allowImportsFrom: [
        "{sharedImports}", // Other shared folders
        "{uiImports}", // UI layer
      ],
      errorMessage:
        "πŸ”₯ Shared folders are not allowed to import items from the `features` folder. πŸ”₯",
    },

    // Layer 1: UI (Most Restricted)
    {
      name: "UI",
      pattern: ["src/components/ui/*", "src/styles/**", "src/lib/*"],
      allowImportsFrom: [
        "{uiImports}", // Only other UI components
      ],
    },

    // Guard: Catch unknown files
    {
      name: "Unknown files",
      pattern: [
        ["src/**", "!src/*"], // All files except root level
      ],
      allowImportsFrom: [],
      allowExternalImports: false,
      errorMessage:
        "πŸ”₯ This file is not specified as an independent module in `independentModules.jsonc`. πŸ”₯",
    },
  ],

  // Reusable patterns for DRY configuration
  reusableImportPatterns: {
    sharedImports: [
      "src/components/shared/**",
      "src/lib/*/**",
      "src/config/**",
      "src/types/**",
      "src/store/**",
      "src/utils/**",
    ],
    uiImports: ["src/components/ui/*", "src/styles/**", "src/lib/*"],
    neverImports: [
      "src/*", // Root level files (except middleware, etc.)
    ],
  },
});

Complete next.config.ts

import type { NextConfig } from "next";
import bundleAnalyzer from "@next/bundle-analyzer";
import path from "path";
import { readdirSync } from "fs";

// Bundle Analyzer Configuration
const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === "true",
});

// Dynamic Feature Discovery
const featuresDir = path.resolve(__dirname, "src/features");
const featureFolders = readdirSync(featuresDir).map(
  (folder) => `@/features/${folder}`
);

// Next.js Configuration
const nextConfig: NextConfig = {
  // Enable React Strict Mode for better development experience
  reactStrictMode: true,

  // Image Optimization Configuration
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "randomuser.me",
      },
      // Add more patterns as needed
      // {
      //   protocol: 'https',
      //   hostname: 'your-cdn.com',
      // },
    ],
  },

  // Experimental Features
  experimental: {
    // Automatic barrel file optimization for all features
    // This enables tree-shaking and code splitting per feature
    optimizePackageImports: [...featureFolders],
  },
};

export default withBundleAnalyzer(nextConfig);

Package.json Scripts Reference

Add these helpful scripts to your package.json:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "lint:fix": "next lint --fix",
    "type-check": "tsc --noEmit",
    "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
    "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
    "build:analyze": "ANALYZE=true next build",
    "clean": "rm -rf .next node_modules",
    "check-all": "npm run type-check && npm run lint && npm run format:check"
  }
}

πŸŽ“ Advanced Patterns

Pattern 1: Feature Flags

Centralized feature flag management:

// config/features.ts
export const FEATURES = {
  ENABLE_NEW_DASHBOARD:
    process.env.NEXT_PUBLIC_FEATURE_NEW_DASHBOARD === "true",
  ENABLE_DARK_MODE: process.env.NEXT_PUBLIC_FEATURE_DARK_MODE === "true",
  ENABLE_ANALYTICS: process.env.NEXT_PUBLIC_FEATURE_ANALYTICS === "true",
} as const;

// utils/featureFlags.ts
import { FEATURES } from "@/config/features";

export function isFeatureEnabled(feature: keyof typeof FEATURES): boolean {
  return FEATURES[feature];
}

// app/dashboard/page.tsx
import { isFeatureEnabled } from "@/utils/featureFlags";
import { DashboardV1 } from "@/features/dashboard-v1";
import { DashboardV2 } from "@/features/dashboard-v2";

export default function DashboardPage() {
  if (isFeatureEnabled("ENABLE_NEW_DASHBOARD")) {
    return <DashboardV2 />;
  }

  return <DashboardV1 />;
}

Pattern 2: Shared Types

Maintain consistency across features:

// types/common.ts
export type Timestamp = {
  createdAt: Date;
  updatedAt: Date;
};

export type WithId<T> = T & { id: string };

export type ApiResponse<T> = {
  data: T;
  meta: {
    page: number;
    total: number;
  };
};

export type AsyncState<T> = {
  data: T | null;
  loading: boolean;
  error: Error | null;
};

// features/products/types.ts
import type { Timestamp, WithId } from "@/types/common";

export type Product = WithId<{
  name: string;
  price: number;
  description: string;
}> &
  Timestamp;

// features/orders/types.ts
import type { Timestamp, WithId } from "@/types/common";

export type Order = WithId<{
  productIds: string[];
  total: number;
  status: "pending" | "completed" | "cancelled";
}> &
  Timestamp;

Pattern 3: Environment Variables

Type-safe environment variable management:

// config/env.ts
import { z } from "zod";

const envSchema = z.object({
  // Public (Client-side)
  NEXT_PUBLIC_API_URL: z.string().url(),
  NEXT_PUBLIC_APP_NAME: z.string(),

  // Private (Server-side only)
  DATABASE_URL: z.string().url(),
  API_SECRET_KEY: z.string().min(32),
  STRIPE_SECRET_KEY: z.string(),
});

// Validate environment variables at build time
const env = envSchema.parse(process.env);

export const ENV = {
  // Public
  apiUrl: env.NEXT_PUBLIC_API_URL,
  appName: env.NEXT_PUBLIC_APP_NAME,

  // Private (only accessible on server)
  databaseUrl: env.DATABASE_URL,
  apiSecretKey: env.API_SECRET_KEY,
  stripeSecretKey: env.STRIPE_SECRET_KEY,
} as const;

// Usage
// lib/api/client.ts
import { ENV } from "@/config/env";

export const apiClient = new ApiClient({
  baseURL: ENV.apiUrl, // βœ… Type-safe, validated
});

Pattern 4: Error Boundaries

Feature-level error isolation:

// components/shared/ErrorBoundary.tsx
"use client";

import { Component, ReactNode } from "react";

type ErrorBoundaryProps = {
  children: ReactNode;
  fallback?: ReactNode;
  onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
};

type ErrorBoundaryState = {
  hasError: boolean;
  error?: Error;
};

export class ErrorBoundary extends Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    this.props.onError?.(error, errorInfo);
    console.error("Error caught by boundary:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <div className="p-4 bg-red-50 border border-red-200 rounded">
            <h2 className="text-red-800 font-semibold">Something went wrong</h2>
            <p className="text-red-600">{this.state.error?.message}</p>
          </div>
        )
      );
    }

    return this.props.children;
  }
}

// app/products/page.tsx
import { ErrorBoundary } from "@/components/shared/ErrorBoundary";
import { ProductList } from "@/features/products";

export default function ProductsPage() {
  return (
    <ErrorBoundary fallback={<div>Failed to load products</div>}>
      <ProductList />
    </ErrorBoundary>
  );
}

Pattern 5: Loading States

Shared loading components:

// components/shared/LoadingState.tsx
import { Loader2 } from "@/components/ui/icons";

type LoadingStateProps = {
  message?: string;
  fullScreen?: boolean;
};

export function LoadingState({
  message = "Loading...",
  fullScreen = false,
}: LoadingStateProps) {
  const className = fullScreen
    ? "fixed inset-0 flex items-center justify-center bg-background/80"
    : "flex items-center justify-center p-8";

  return (
    <div className={className}>
      <div className="flex flex-col items-center gap-2">
        <Loader2 className="h-8 w-8 animate-spin" />
        <p className="text-muted-foreground">{message}</p>
      </div>
    </div>
  );
}

// features/products/components/ProductList.tsx
import { LoadingState } from "@/components/shared/LoadingState";
import { useProducts } from "../hooks/useProducts";

export function ProductList() {
  const { data: products, loading, error } = useProducts();

  if (loading) return <LoadingState message="Loading products..." />;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

Pattern 6: Testing Strategy

Feature-level testing:

// features/products/hooks/useProducts.test.ts
import { renderHook, waitFor } from "@testing-library/react";
import { useProducts } from "./useProducts";
import { fetchProducts } from "../api/productService";

// Mock the API service
jest.mock("../api/productService");

describe("useProducts", () => {
  it("should fetch products on mount", async () => {
    const mockProducts = [
      { id: "1", name: "Product 1", price: 100 },
      { id: "2", name: "Product 2", price: 200 },
    ];

    (fetchProducts as jest.Mock).mockResolvedValue(mockProducts);

    const { result } = renderHook(() => useProducts());

    expect(result.current.loading).toBe(true);

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
      expect(result.current.data).toEqual(mockProducts);
    });
  });

  it("should handle errors", async () => {
    const mockError = new Error("Failed to fetch");
    (fetchProducts as jest.Mock).mockRejectedValue(mockError);

    const { result } = renderHook(() => useProducts());

    await waitFor(() => {
      expect(result.current.error).toEqual(mockError);
    });
  });
});

// features/products/components/ProductCard.test.tsx
import { render, screen } from "@testing-library/react";
import { ProductCard } from "./ProductCard";

describe("ProductCard", () => {
  const mockProduct = {
    id: "1",
    name: "Test Product",
    price: 99.99,
    image: "/test.jpg",
  };

  it("should render product details", () => {
    render(<ProductCard product={mockProduct} />);

    expect(screen.getByText("Test Product")).toBeInTheDocument();
    expect(screen.getByText("$99.99")).toBeInTheDocument();
  });

  it("should call onAddToCart when button clicked", () => {
    const onAddToCart = jest.fn();
    render(<ProductCard product={mockProduct} onAddToCart={onAddToCart} />);

    const button = screen.getByRole("button", { name: /add to cart/i });
    button.click();

    expect(onAddToCart).toHaveBeenCalledWith(mockProduct.id);
  });
});

πŸš€ Migration Guide

Migrating from Unstructured to This Architecture

Step 1: Analyze Current Structure

# List all components
find src -name "*.tsx" -o -name "*.ts" | grep -v node_modules

# Identify features
# Look for related files that should be grouped together

Step 2: Create Feature Folders

mkdir -p src/features/{auth,products,dashboard,profile}

# For each feature:
mkdir -p src/features/auth/{components,hooks,api,utils,types}

Step 3: Move Files Gradually

Start with one feature:

# Move auth-related files
git mv src/components/LoginForm.tsx src/features/auth/components/
git mv src/components/SignupForm.tsx src/features/auth/components/
git mv src/hooks/useAuth.ts src/features/auth/hooks/
git mv src/api/authService.ts src/features/auth/api/

Step 4: Create Index Files

// src/features/auth/index.ts
export { LoginForm } from "./components/LoginForm";
export { SignupForm } from "./components/SignupForm";
export { useAuth } from "./hooks/useAuth";
export type { User, AuthState } from "./types";

Step 5: Update Imports

# Find all imports that need updating
grep -r "import.*from.*components/LoginForm" src/

# Update them to use the new structure
# Before: import { LoginForm } from '@/components/LoginForm';
# After: import { LoginForm } from '@/features/auth';

Step 6: Add Configuration

  1. Add TypeScript paths (already done in tsconfig.json)
  2. Add ESLint rules (eslint.config.mjs + independentModuleConfig.mjs)
  3. Add Next.js optimization (next.config.ts)

Step 7: Verify

npm run type-check
npm run lint
npm run build

Step 8: Repeat for Each Feature

Migrate one feature at a time, testing after each migration.


πŸ“Š Architecture Decision Records (ADRs)

ADR 1: Why Feature-Based Architecture?

Context: As applications grow, organizing by technical type (components/, hooks/, utils/) becomes difficult to navigate and maintain.

Decision: Organize code by business domain (features/) instead of technical type.

Consequences:

  • βœ… Features are self-contained and easier to understand
  • βœ… Related code is co-located
  • βœ… Easier to onboard new developers
  • βœ… Can extract features to separate packages if needed
  • ⚠️ Requires discipline to maintain boundaries
  • ⚠️ Initial setup takes more time

ADR 2: Why Enforce with ESLint?

Context: Developers might accidentally create cross-feature dependencies, breaking the architecture.

Decision: Use eslint-plugin-project-structure to enforce import rules automatically.

Consequences:

  • βœ… Catches violations immediately during development
  • βœ… Prevents architectural decay
  • βœ… Makes code reviews easier
  • βœ… Self-documenting architecture
  • ⚠️ Can be frustrating initially for developers
  • ⚠️ Requires clear documentation

ADR 3: Why Dynamic Feature Discovery?

Context: Manually updating next.config.ts when adding features is error-prone.

Decision: Automatically read src/features/ directory at build time.

Consequences:

  • βœ… Zero configuration when adding features
  • βœ… No manual updates needed
  • βœ… Less prone to mistakes
  • ⚠️ Depends on filesystem at build time
  • ⚠️ Requires consistent folder structure

ADR 4: Why Prefer Type Aliases Over Interfaces?

Context: TypeScript has two ways to define object types.

Decision: Enforce type aliases with @typescript-eslint/consistent-type-definitions.

Consequences:

  • βœ… More flexible (unions, intersections)
  • βœ… Consistent codebase style
  • βœ… Better for composition
  • ⚠️ Slight learning curve for interface users

🎯 Best Practices Checklist

Before Creating a New Feature

  • Feature name clearly represents a business domain
  • Feature is independent enough to stand alone
  • No overlap with existing features
  • Clear public API planned

When Creating a Feature

  • Create proper folder structure (components/, hooks/, api/, etc.)
  • Create index.ts with selective exports
  • Keep internal utilities private
  • Add types.ts for TypeScript definitions
  • Only import from allowed layers

Before Moving Code to Shared

  • Used by 2+ features
  • No feature-specific logic
  • Has clear, general purpose
  • Properly tested

Before Committing

  • npm run type-check passes
  • npm run lint passes
  • npm run build succeeds
  • No new ESLint violations
  • Bundle size is reasonable

Code Review Checklist

  • Follows layer hierarchy
  • No cross-feature imports
  • Public APIs are minimal
  • Shared code is truly shared
  • No business logic in UI components
  • Types are properly exported

🎬 Quick Start Checklist

Setting up a new project with this architecture:

1. Initialize Project

npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app

2. Install Dependencies

npm install -D \
  eslint-plugin-project-structure \
  @typescript-eslint/parser \
  @typescript-eslint/eslint-plugin \
  eslint-config-prettier \
  eslint-plugin-prettier \
  prettier

3. Create Folder Structure

mkdir -p src/{features,components/{shared,ui},lib,config,types,store,utils,styles}

4. Add Configuration Files

Copy the configuration files from this guide:

  • tsconfig.json
  • eslint.config.mjs
  • independentModuleConfig.mjs
  • next.config.ts

5. Create Your First Feature

mkdir -p src/features/auth/{components,hooks,api,types}
touch src/features/auth/index.ts

6. Verify Setup

npm run lint
npm run type-check
npm run build

πŸ“– Further Reading

Recommended Resources

Related Patterns

  • Vertical Slice Architecture: Similar approach focusing on business capabilities
  • Modular Monolith: Architecture pattern this implements
  • Domain-Driven Design: Inspiration for feature boundaries
  • Hexagonal Architecture: Ports and adapters pattern influence

πŸŽ‰ Conclusion

You now have a complete understanding of this Next.js architecture! Here's what makes it special:

The Three Pillars

  1. TypeScript - Clean imports with path aliases
  2. ESLint - Enforced architectural boundaries
  3. Next.js - Optimized bundle sizes

Key Takeaways

  • 🎯 Features are independent domains
  • πŸ”’ Layers enforce separation of concerns
  • πŸ“¦ Bundle optimization happens automatically
  • 🚦 ESLint catches violations immediately
  • πŸ“ TypeScript ensures type safety

Remember

"Architecture is about making the important things easy and the wrong things hard."

This architecture makes:

  • βœ… Easy: Adding features, maintaining code, scaling teams
  • ❌ Hard: Creating coupling, breaking boundaries, writing spaghetti code

πŸ™‹ FAQ

Q: Can a feature have sub-features? A: No, keep features flat. If you need sub-grouping, it might be a sign to split into separate features or reconsider the domain boundaries.

Q: What if I need to share code between two features? A: Move it to the shared layer or pass data through the app layer. Cross-feature imports are prohibited.

Q: Can I use this with Pages Router? A: Yes! The principles apply equally. Just replace app/ with pages/ in the configuration.

Q: How do I handle global state? A: Use the store/ folder with your preferred state management (Zustand, Redux, etc.). Features can consume from the store.

Q: What about utility functions used in only one feature? A: Keep them in that feature's utils/ folder. Only move to shared utils/ when used by multiple features.

Q: Can I nest features? A: No, keep features flat for simplicity. Use clear naming instead: user-management, user-authentication, etc.

Q: How do I test this architecture? A: Test features in isolation. The enforced boundaries make testing easier since dependencies are explicit.


Happy coding! πŸš€

Remember: Good architecture is invisible when it works and obvious when it doesn't. This architecture works.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published