Every Next.js project I built used to end up the same way. Fifty files in the root, components scattered everywhere, and me spending more time finding things than building them.

The App Router made it worse at first because suddenly you had page.tsx, layout.tsx, loading.tsx, and error.tsx files everywhere. My project looked organized on the surface but I couldn’t find anything when I needed it.

After building probably a dozen Next.js apps over the last couple of years, I finally have a structure I don’t hate. It’s not perfect. But I never have to think about where to put things anymore, and that’s what matters.


The top-level layout

Here’s what my project root looks like:

src/
  app/
  components/
  lib/
  hooks/
  types/
  constants/

Everything lives inside src/. I know some people put things at the root level but I like having a clean separation between config files and actual code.


The app directory is only for routes

This is the rule that changed everything for me. The app/ directory should only contain routing-related files. Pages, layouts, loading states, error boundaries. Nothing else.

app/
  (auth)/
    login/
      page.tsx
    register/
      page.tsx
  dashboard/
    page.tsx
    layout.tsx
    settings/
      page.tsx
  api/
    users/
      route.ts
    posts/
      route.ts
  layout.tsx
  page.tsx

I used to put components inside route folders. Like app/dashboard/components/Chart.tsx. Don’t do this. It seems organized until you need that chart somewhere else and suddenly you’re importing from @/app/dashboard/components/Chart which feels wrong.


Components get their own structure

components/
  ui/
    Button.tsx
    Input.tsx
    Modal.tsx
    Card.tsx
  forms/
    LoginForm.tsx
    SettingsForm.tsx
  layout/
    Header.tsx
    Sidebar.tsx
    Footer.tsx

Three folders. That’s it. ui/ is for generic reusable stuff. forms/ is for form components that handle specific data. layout/ is for the shell of the app.

I don’t create a folder per component unless it has multiple files. A Button.tsx doesn’t need a Button/ folder with an index.ts that re-exports it. That’s just extra clicks for no reason.


The lib folder is where the real work happens

This is where I put all the business logic, API calls, and utilities.

lib/
  api/
    users.ts
    posts.ts
  db/
    prisma.ts
    queries/
      users.ts
      posts.ts
  utils/
    format.ts
    validation.ts
  auth.ts

lib/api/ has functions that call external APIs or wrap my own API routes. lib/db/ has the Prisma client and any database queries. lib/utils/ has pure utility functions.

The important thing is that components never talk to the database directly. A page component calls a function from lib/, and that function handles the data fetching. This keeps the data layer separate from the UI layer.

// lib/db/queries/users.ts
import { prisma } from "@/lib/db/prisma";

export async function getUserById(id: string) {
  return prisma.user.findUnique({
    where: { id },
    select: { id: true, name: true, email: true },
  });
}
// app/dashboard/page.tsx
import { getUserById } from "@/lib/db/queries/users";

export default async function DashboardPage() {
  const user = await getUserById("some-id");
  return <Dashboard user={user} />;
}

Clean. The page doesn’t know or care how the data is fetched.


Server vs client components

This tripped me up for a while. Here’s how I think about it now:

Server components by default. Every component is a server component unless it needs interactivity. If it just displays data, it stays on the server.

Client components only when necessary. If it uses useState, useEffect, event handlers, or browser APIs, add "use client" at the top.

The mistake I kept making was putting "use client" too high in the component tree. If your page layout is a client component, everything inside it becomes a client component too. That kills performance.

Instead, push "use client" as deep as possible:

// This stays as a server component
export default async function DashboardPage() {
  const stats = await getStats();

  return (
    <div>
      <h1>Dashboard</h1>
      <StatsDisplay stats={stats} />
      <ActivityChart /> {/* only this needs to be client */}
    </div>
  );
}
// components/ActivityChart.tsx
"use client";

import { useState } from "react";

export function ActivityChart() {
  const [range, setRange] = useState("week");
  // interactive chart logic here
}

Only the chart is a client component. Everything else renders on the server.


Route groups for organization

Route groups with parentheses are one of my favorite Next.js features. They let you organize routes without affecting the URL.

app/
  (marketing)/
    page.tsx          → /
    about/page.tsx    → /about
    pricing/page.tsx  → /pricing
    layout.tsx        → shared marketing layout
  (app)/
    dashboard/page.tsx → /dashboard
    settings/page.tsx  → /settings
    layout.tsx         → shared app layout (with sidebar)
  (auth)/
    login/page.tsx     → /login
    register/page.tsx  → /register
    layout.tsx         → centered auth layout

Each group gets its own layout. The marketing pages have a simple header and footer. The app pages have a sidebar. The auth pages are centered on the screen. And the URLs stay clean.


Hooks and types

hooks/
  useDebounce.ts
  useLocalStorage.ts
  useMediaQuery.ts

types/
  user.ts
  post.ts
  api.ts

Hooks get their own folder. Types get their own folder. I don’t co-locate these with components because I usually reuse them across multiple parts of the app.

For types, I keep one file per domain. user.ts has all user-related types. api.ts has generic API response types. Simple.


Path aliases

Always set up path aliases. Going three directories up with ../../../ is painful and makes refactoring a nightmare.

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

Now every import starts with @/:

import { Button } from "@/components/ui/Button";
import { getUserById } from "@/lib/db/queries/users";
import { useDebounce } from "@/hooks/useDebounce";

Clean and you can move files around without fixing a chain of relative imports.


What I stopped doing

A few things I used to do that I dropped:

  • Barrel files (index.ts exports) — More trouble than they’re worth. They mess with tree shaking and make it harder to find where things are defined.
  • One component per folder — Unless a component has its own styles, tests, and utils, a single file is fine.
  • Separating by feature — I tried the “feature folders” approach where each feature has its own components, hooks, and utils. It sounds good in theory but in practice I kept duplicating shared code.
  • Putting everything in utils/ — If your utils folder has 30 files, it’s not a utils folder anymore. Be specific.

The point isn’t the structure

The real goal is that you never waste time thinking about where to put a new file or where to find an existing one. If your current structure does that, keep it.

But if you’re spending more time navigating than coding, try this layout for your next project. It’s boring and predictable, which is exactly what a folder structure should be.