Understanding App Router File Conventions
The App Router's power comes from its convention over configuration design. Instead of manually defining routes in a config file, you create files with specific names in your app directory, and Next.js automatically recognizes them and generates your application's routes, layouts, API endpoints, and more. Understanding these conventions is essential; they form the vocabulary of modern Next.js development.
The Core File Conventions
The App Router recognizes specific filenames, each with a dedicated purpose. When Next.js sees these files in a folder, it interprets them as route configuration.
page.tsx – Rendering Route Content
page.tsx defines the UI rendered at a route. It's the only file that makes a route publicly accessible. Create a file at app/dashboard/page.tsx, and you've created a /dashboard route.
// app/dashboard/page.tsx
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<p>Welcome back, user!</p>
</div>
);
}
When a user navigates to /dashboard, Next.js renders this component. A folder without a page.tsx file is not a publicly accessible route—it's just a container for sub-routes. For example, a folder app/admin/ with no page.tsx is not viewable; only app/admin/users/page.tsx (a /admin/users route) is accessible.
layout.tsx – Creating Wrapper Components
layout.tsx defines a layout component that wraps all child routes in a folder and its subfolders. Layouts persist during navigation—if you navigate from /dashboard to /dashboard/users, the layout doesn't unmount or re-render, only the inner page.tsx swaps. This is unlike pages, where navigation causes a full re-render.
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<nav className="sidebar">
<a href="/dashboard">Home</a>
<a href="/dashboard/users">Users</a>
<a href="/dashboard/settings">Settings</a>
</nav>
<main>{children}</main>
</div>
);
}
The children prop is the content of child pages. When you visit /dashboard/users, Next.js renders: DashboardLayout → DashboardUsersPage. The layout wraps the page. This pattern is powerful for shared UI like sidebars, headers, and breadcrumbs that shouldn't re-render when you navigate within a section.
Every App Router app has a root layout at app/layout.tsx. This layout wraps every page in the entire application. It must define <html> and <body> tags.
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<title>My App</title>
<meta name="description" content="A great app" />
</head>
<body>
<header>
<h1>My App</h1>
</header>
{children}
<footer>
<p>© 2026. All rights reserved.</p>
</footer>
</body>
</html>
);
}
route.ts – Handling API Requests
route.ts defines an API route handler. Instead of code in a separate pages/api/ folder or a backend server, you write HTTP handlers in your Next.js app. Each route.ts file exports functions for HTTP methods: GET, POST, PUT, DELETE, etc.
// app/api/posts/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const posts = await fetch("https://jsonplaceholder.typicode.com/posts").then(
(r) => r.json()
);
return NextResponse.json(posts);
}
export async function POST(request: NextRequest) {
const body = await request.json();
// Save to database
return NextResponse.json({ created: true, data: body }, { status: 201 });
}
When a client sends a GET request to /api/posts, the GET function runs and returns JSON. POST requests trigger the POST function. No server framework needed—Next.js handles it natively.
loading.tsx – Suspense Boundaries
loading.tsx defines a UI shown while content is loading, powered by React Suspense. Create a loading.tsx file in a folder, and Next.js wraps that folder's page.tsx in a Suspense boundary. While the page loads, the loading UI displays; once the page resolves, it replaces the loader.
// app/blog/loading.tsx
export default function BlogLoading() {
return (
<div className="animate-pulse">
<div className="h-10 bg-gray-300 rounded mb-4"></div>
<div className="h-6 bg-gray-300 rounded mb-2"></div>
<div className="h-6 bg-gray-300 rounded w-5/6"></div>
</div>
);
}
When you visit /blog, you'll see skeleton placeholders while the blog page data loads. Once ready, the actual blog page replaces the skeleton. This pattern improves perceived performance and user experience, especially on slow networks.
error.tsx – Error Boundaries
error.tsx defines an error boundary that catches errors in child routes. If a component in your page throws an error, instead of crashing the whole app, error.tsx displays a custom error UI.
// app/blog/error.tsx
"use client";
export default function BlogError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
If a blog post fails to load, users see this error UI with a "Try again" button instead of a blank white screen. The reset function re-renders the page, allowing recovery without a full page reload.
File Convention Hierarchy
Here's a complete folder structure showing how conventions stack:
app/
├── layout.tsx # Root layout (every page)
├── page.tsx # Home page at /
├── error.tsx # Catches errors in root and children
├── loading.tsx # Suspense loader for root and children
├── blog/
│ ├── layout.tsx # Wraps all /blog/* routes
│ ├── page.tsx # /blog homepage
│ ├── loading.tsx # Loader for /blog
│ ├── error.tsx # Error boundary for /blog
│ └── [slug]/
│ ├── page.tsx # /blog/:slug (dynamic)
│ └── layout.tsx # Wraps /blog/:slug
├── api/
│ └── posts/
│ └── route.ts # POST/GET /api/posts
└── not-found.tsx # Custom 404 page (optional)
Navigation from /blog to /blog/my-post preserves the /blog/layout.tsx (no re-render), but swaps the page component.
Special Files Summary
| File | Purpose | Example Location |
|---|---|---|
page.tsx | Render public route | app/dashboard/page.tsx |
layout.tsx | Wrap child routes with shared UI | app/dashboard/layout.tsx |
route.ts | API endpoint handler | app/api/posts/route.ts |
loading.tsx | Suspense placeholder during load | app/blog/loading.tsx |
error.tsx | Error boundary for child routes | app/blog/error.tsx |
not-found.tsx | Custom 404 page | app/not-found.tsx |
template.tsx | Re-renders on navigation (rare) | app/layout-sidebar.tsx |
Key Takeaways
page.tsxmakes a route publicly accessible; folders without it are just containers.layout.tsxwraps child routes and persists during navigation, ideal for shared UI like sidebars.route.tshandles API requests; exportGET,POST,PUT,DELETEfunctions.loading.tsxshows a Suspense placeholder while pages load, improving perceived performance.error.tsxcatches errors in child routes and displays a recovery UI.- These conventions work together: a route renders inside its nearest layout, inside an error boundary, with a loading state if async.
Frequently Asked Questions
What happens if I create a folder with no page.tsx?
The folder becomes a route segment (a URL path) but isn't publicly accessible. You can use it to organize child routes—for example, app/admin/ (no page) can contain app/admin/users/page.tsx (/admin/users) and app/admin/settings/page.tsx (/admin/settings). The admin folder itself has no page.
Can I have both a page.tsx and an error.tsx in the same folder?
Yes. The error.tsx wraps the page.tsx in an error boundary. If page.tsx throws, error.tsx catches it. They're not mutually exclusive; they work together.
Do loading.tsx and page.tsx need to be in the same folder?
Yes. Next.js only shows the loading UI if it's in the same folder as (or above) the page. A loading.tsx at app/blog/loading.tsx wraps app/blog/page.tsx but not app/blog/posts/page.tsx—that needs its own loader at app/blog/posts/loading.tsx.
Can I use .js or .jsx instead of .tsx?
Yes, the App Router supports .js, .jsx, .ts, and .tsx. Use .tsx for components (exports JSX) and .ts for pure JavaScript (exports functions, like API routes). TypeScript is optional but recommended.
What's the difference between layout.tsx and template.tsx?
layout.tsx persists and doesn't re-render on navigation within its folder—ideal for sidebars and headers. template.tsx re-renders on every navigation—useful for animations or state reset. Layouts are far more common; templates are rarely needed.
Further Reading
- Next.js App Router File Conventions – official comprehensive reference.
- React Suspense Documentation – deep dive into loading boundaries.
- Next.js API Routes Guide – complete API route documentation.