Organize Next.js projects for maintainability and performance.
Create routes by adding a folder and a page.tsx file inside it.
app/
page.tsx # / route
about/
page.tsx # /about route
blog/
page.tsx # /blog route
[slug]/
page.tsx # /blog/:slug route
Map URL path segments directly to folder names in the app directory.
app/users/[id]/posts/page.tsx → /users/:id/posts
Add a root layout.tsx to wrap every page with shared HTML structure.
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <html lang="en"><body>{children}</body></html>;
}
Use page.tsx for visible UI and route.ts for API route handlers.
app/
api/
users/
route.ts # GET /api/users, POST /api/users
users/
page.tsx # /users page rendered in the browser
Add a loading.tsx next to any page to show a Suspense fallback.
// app/blog/loading.tsx
export default function Loading() {
return <p>Loading posts...</p>;
}
Use dynamic [slug] segments to match variable URL parts — in Next.js 15, params is a Promise.
// app/blog/[slug]/page.tsx
export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
return <h1>Post: {slug}</h1>;
}
Use layout.tsx to share persistent UI across child routes without remounting.
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
Use error.tsx as an error boundary — it must be a Client Component.
// app/blog/error.tsx
"use client";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Something went wrong</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
Use loading.tsx to create an automatic Suspense boundary for the segment.
// app/blog/loading.tsx
export default function Loading() {
return <p>Loading posts...</p>;
}
Use not-found.tsx to render a custom 404 for each route segment.
// app/blog/[slug]/not-found.tsx
export default function NotFound() {
return <h1>Post not found</h1>;
}
Use template.tsx when the layout must remount on each navigation.
// app/dashboard/template.tsx
export default function Template({ children }: { children: React.ReactNode }) {
// Re-runs useEffect and re-mounts on every route change
return <div>{children}</div>;
}
Wrap folder names in parentheses to exclude them from the URL path.
app/
(marketing)/
page.tsx # / (still at root URL)
about/page.tsx # /about
contact/page.tsx # /contact
(dashboard)/
layout.tsx # separate layout, no URL impact
page.tsx # /dashboard
Give each route group its own layout for auth vs app separation.
app/
(auth)/
layout.tsx # auth layout with centered card
login/page.tsx # /login
signup/page.tsx # /signup
(app)/
layout.tsx # app layout with sidebar nav
dashboard/page.tsx
Scope middleware-like access control by grouping protected routes.
app/
(protected)/
dashboard/page.tsx
settings/page.tsx
(public)/
page.tsx
about/page.tsx
Define a layout per group without affecting the URL structure.
// app/(auth)/layout.tsx
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return <main className="auth-bg">{children}</main>;
}
Combine route groups with parallel routes using the @ prefix.
app/
(dashboard)/
@analytics/page.tsx
@overview/page.tsx
layout.tsx
Use TypeScript path aliases to avoid deep relative imports across the project.
// tsconfig.json
{
"compilerOptions": {
"paths": { "@/*": ["./src/*"] }
}
}
Create barrel files with index.ts to expose a clean public API for each feature.
// features/auth/index.ts
export { LoginForm } from "./components/LoginForm";
export { useAuth } from "./hooks/useAuth";
export type { AuthUser } from "./types";
Mark server-only modules with the server-only package to prevent client imports.
// lib/db.ts
import "server-only";
export const db = new PrismaClient();
Use a global types/ folder for shared TypeScript interfaces across features.
types/
api.ts # shared API response shapes
user.ts # User interface used app-wide
post.ts # Post interface used app-wide
Re-export shared UI primitives from a single index to keep import paths short.
app/
ui/
Button.tsx
Card.tsx
Modal.tsx
index.ts # export { Button, Card, Modal }
Use feature folders to group all files for one domain in one place.
app/
features/
auth/
page.tsx
components/
hooks/
utils.ts
posts/
page.tsx
components/
hooks/
Keep all Next.js special files inside the feature folder.
features/users/
page.tsx
layout.tsx
error.tsx
loading.tsx
components/
UserCard.tsx
hooks/
useUser.ts
Create barrel files with index.ts to simplify imports from feature folders.
// features/auth/index.ts
export { LoginForm } from "./components/LoginForm";
export { useAuth } from "./hooks/useAuth";
Place server actions in a dedicated actions/ folder inside each feature.
features/posts/
actions/
createPost.ts
deletePost.ts
components/
page.tsx
Put cross-feature utilities in a root lib/ folder accessible to all routes.
lib/
format.ts # shared date/number formatting
fetcher.ts # shared fetch wrapper
constants.ts # app-wide constants
Page-specific components should live next to the route they belong to, inside the same folder. Shared components used across multiple routes go in a top-level 'components' directory or a '_components' folder inside 'app'.
Folders wrapped in parentheses like '(marketing)' are excluded from the URL path but still organize your file system. Use them to group routes by section or layout without affecting the actual routes users see.
Use the 'app' directory — it's the current standard and enables React Server Components, nested layouts, and streaming. The 'pages' router is still supported but is considered legacy for new projects.
'layout.tsx' persists across navigations and doesn't remount, making it ideal for sidebars and headers. 'template.tsx' creates a fresh instance on every navigation, so use it when you need effects or animations to re-run between routes.
Yes — only files named as special files (page.tsx, layout.tsx, etc.) are treated as routes. You can safely place hooks, utilities, and components alongside your route files and they won't become endpoints.