Project Structure

📖 Introduction

Understanding how a Next.js project is organized is essential for building maintainable, scalable applications. This tutorial provides a comprehensive breakdown of every important file and folder you'll encounter — from the root configuration files to deeply nested feature folders.

You'll learn what each directory is for, which files Next.js treats as special, and how to organize your own custom folders like components, lib, and hooks as your project grows. đŸ—‚ī¸

Information

This tutorial assumes you already have a Next.js project created using the App Router.

🧭 Understanding the Project Structure

A Next.js project is composed of conventional files and folders (which Next.js recognizes automatically) and custom folders (which you create for your own organization). Understanding this distinction is key to structuring a project well.

Next.js Project
Conventional (framework-recognized)
Custom (developer-organized)
app/
public/
middleware.ts
components/
lib/
hooks/

📂 Root Directory

The root directory contains your project's configuration files, top-level folders, and package metadata. Here's a typical layout for a well-organized project:

my-app
app
layout.tsx
page.tsx
public
middleware.ts
instrumentation.ts
next.config.ts
package.json
tsconfig.json
eslint.config.js
.env

📁 The app Directory

The app directory is the core routing engine of the App Router. Every folder inside it can represent a route segment, and special files within those folders define UI, layouts, loading states, and more.

The public Directory

Static files such as images, fonts, and favicons are placed here. Anything inside public is served from the root URL — for example, public/logo.png becomes accessible at /logo.png.

The src Directory

An optional top-level folder that houses your application source code, keeping it separate from root-level config files like next.config.ts and package.json.

Tip

If you use src/, your app directory moves to src/app.

The components Directory

Holds reusable UI components shared across pages and features — buttons, modals, cards, form inputs, and layout pieces.

components
Button.tsx
Card.tsx
Modal.tsx

The lib Directory

Contains library code and third-party client configurations — database connections, API clients, authentication setup, and similar integrations.

lib/db.ts

import { PrismaClient } from "@prisma/client";

export const db = new PrismaClient();

The hooks Directory

Stores custom React hooks that encapsulate reusable stateful logic across components.

hooks/useDebounce.ts

import { useEffect, useState } from "react";

export function useDebounce<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

The styles Directory

Houses global stylesheets, Tailwind configuration extensions, and shared CSS modules not tied to a specific component.

The types Directory

Contains shared TypeScript type definitions and interfaces used across multiple parts of the application.

types/user.ts

export interface User {
  id: string;
  name: string;
  email: string;
}

The utils Directory

Holds small, pure utility functions — formatters, validators, and helpers that don't belong to any specific feature.

utils/formatDate.ts

export function formatDate(date: Date): string {
  return new Intl.DateTimeFormat("en-US").format(date);
}

The constants Directory

Centralizes fixed values used throughout the app — configuration constants, enum-like values, and shared strings.

constants/routes.ts

export const ROUTES = {
  HOME: "/",
  DASHBOARD: "/dashboard",
} as const;

The services Directory

Encapsulates logic for communicating with external APIs or backend services, keeping network logic separate from UI code.

services/userService.ts

export async function getUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

The actions Directory

Holds Server Actions — functions marked with "use server" that run exclusively on the server and can be called directly from client components.

actions/createPost.ts

"use server";

export async function createPost(formData: FormData) {
  const title = formData.get("title");
  // Save to database...
}

🧩 The middleware File

The middleware.ts file runs code before a request completes, allowing you to modify responses, handle redirects, or enforce authentication checks.

middleware.ts

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  return NextResponse.next();
}

📊 The instrumentation File

The instrumentation.ts file lets you run setup code when the server starts, commonly used for monitoring and observability tooling.

instrumentation.ts

export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    await import("./instrumentation-node");
  }
}

📄 Special Files in the app Directory

Next.js reserves certain filenames within the app directory for specific purposes, each rendering automatically at the appropriate point in the route lifecycle.

The app/layout.tsx File

Defines shared UI that wraps child pages, such as navigation bars, footers, and providers. The root layout is required.

app/layout.tsx

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

The app/page.tsx File

Defines the unique UI for a route and makes that route publicly accessible.

app/page.tsx

export default function HomePage() {
  return <h1>Welcome Home</h1>;
}

The app/loading.tsx File

Automatically wraps a page in a Suspense boundary, showing a loading UI while the page content streams in.

app/loading.tsx

export default function Loading() {
  return <p>Loading...</p>;
}

The app/error.tsx File

Automatically wraps a route segment in an error boundary, catching runtime errors and displaying fallback UI. It must be a Client Component.

app/error.tsx

"use client";

export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <div>
      <p>Something went wrong!</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

The app/not-found.tsx File

Renders when the notFound() function is called or a route doesn't match any segment, providing a custom 404 experience.

app/not-found.tsx

export default function NotFound() {
  return <h2>Page Not Found</h2>;
}

The app/global-error.tsx File

Catches errors in the root layout itself. Since it replaces the root layout when active, it must include its own <html> and <body> tags.

The app/template.tsx File

Similar to layout.tsx, but creates a new instance for each child route on navigation, useful for re-triggering animations or effects.

The route.ts File

Defines a Route Handler, allowing you to build custom API endpoints using standard Request and Response APIs.

app/api/users/route.ts

import { NextResponse } from "next/server";

export async function GET() {
  return NextResponse.json({ users: [] });
}

Important

A route segment cannot contain both a page.tsx and a route.ts at the same level.

âš™ī¸ Configuration Files

The next.config.ts File

Customizes Next.js's build and runtime behavior, including redirects, headers, image domains, and experimental features.

The package.json File

Defines project metadata, dependencies, and CLI scripts such as dev, build, and start.

The tsconfig.json File

Configures TypeScript compiler options, including path aliases like @/components for cleaner imports.

tsconfig.json

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

The eslint.config.js File

Defines linting rules using ESLint's modern flat config format, ensuring consistent code quality across the codebase.

The .env Files

Store environment-specific variables. Next.js supports multiple variants for different environments:

FilePurpose
.envDefault variables for all environments
.env.localLocal overrides, ignored by git
.env.developmentDevelopment-only variables
.env.productionProduction-only variables

Danger

Never commit files containing secrets — always add .env*.local to .gitignore.

đŸĸ Organizing Large Projects

As applications grow, a flat folder structure becomes harder to navigate. Larger projects benefit from more deliberate organization strategies.

Feature-Based Folder Structure

Instead of grouping files by type (all components together, all hooks together), a feature-based structure groups files by the feature they belong to.

src
features
auth
LoginForm.tsx
useAuth.ts
authService.ts

Best Practice

Feature-based structures scale better for large teams since related code stays co-located rather than scattered across generic folders.

✅ Best Practices

  1. Keep the app directory focused on routing — move business logic into lib, services, or actions.
  2. Use types for shared interfaces instead of duplicating them across files.
  3. Group related components, hooks, and logic together for feature-heavy applications.
  4. Use consistent naming conventions (camelCase for functions, PascalCase for components).
  5. Configure path aliases in tsconfig.json to avoid deep relative imports like ../../../components.

âš ī¸ Common Mistakes

  • Placing business logic directly inside page components instead of lib or services.
  • Mixing page.tsx and route.ts in the same segment, which Next.js does not allow.
  • Forgetting "use client" on interactive components that use state or event handlers.
  • Overusing a single flat components folder for a large, multi-feature application.

❓ Frequently Asked Questions

No — the src directory is optional. It's purely a matter of preference for separating source code from config files.

No — app is a reserved, framework-recognized directory name and cannot be renamed.

Shared, fixed values belong in a dedicated constants directory, separate from utils or types.

đŸŽ¯ Summary

With a solid grasp of Next.js project structure, you're ready to build applications that stay organized and maintainable as they scale. Next, explore routing and data fetching in depth! 🚀