How Next.js App Router Folders, Layouts, and Data-Loading Routes Fit Together
Follow one Next.js App Router example from folder structure to nested layouts, dynamic posts, query-driven data loading, and loading UI.

A useful way to plan an App Router route is to separate three decisions: Which URL identifies the page? Which UI is shared? Which URL values affect its data? Folders answer the first question, layouts answer the second, and page props give a Server Component the parameters it needs for the third.
Turn URLs into folders
Each folder under app represents a URL segment. A page.tsx supplies the page for that path; a folder without a page does not, by itself, make a page publicly accessible. Here is a small blog with an index and individual posts:
app/
layout.tsx → shared root layout
blog/
layout.tsx → shared blog layout
loading.tsx → blog loading UI
posts.ts → example data-loading functions
page.tsx → /blog
[slug]/
page.tsx → /blog/first-post, /blog/another-post, …
The bracketed folder is a dynamic segment: its slug value comes from that part of the path. The posts.ts file is colocated with the routes but does not create another URL. Next.js documents both the segment-to-URL mapping and the distinction between route files and colocated code in its App Router routing guide.
Put shared UI in layouts
The root layout is required and must render <html> and <body>. Both layouts receive their child page—or another layout—through children:
// app/layout.tsx
import type { ReactNode } from 'react';
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<header>Next.js Notes</header>
{children}
</body>
</html>
);
}
// app/blog/layout.tsx
import type { ReactNode } from 'react';
import Link from 'next/link';
export default function BlogLayout({ children }: { children: ReactNode }) {
return (
<section>
<nav aria-label="Blog">
<Link href="/blog">All posts</Link>{' · '}
<Link href="/blog?topic=next">Next.js posts</Link>
</nav>
{children}
</section>
);
}
For either blog URL, the rendered nesting is root layout → blog layout → page. The root header and blog navigation belong to the layouts; the list or individual post belongs to the page. During navigation, shared layouts remain in place rather than rerendering, as described in the layouts and pages documentation.
Use a path for the post, a query for the view
A post slug identifies a resource, so /blog/first-post is a good fit for [slug]. A topic filter changes which posts appear on the index, so /blog?topic=next keeps the same page path and adds a query parameter. Pagination can follow the same query-string pattern.
Pages and layouts are Server Components by default. When a query value determines what data the page loads, read it from the page’s searchParams prop. This example keeps the data source deliberately small so the URL-to-data flow is visible:
// app/blog/posts.ts
const posts = [
{ slug: 'first-post', title: 'First post', topic: 'next' },
{ slug: 'another-post', title: 'Another post', topic: 'react' },
];
export async function loadAllPosts() {
return posts;
}
export async function loadPostsByTopic(topic?: string) {
return topic ? posts.filter((post) => post.topic === topic) : posts;
}
export async function loadPostBySlug(slug: string) {
return posts.find((post) => post.slug === slug);
}
// app/blog/page.tsx
import Link from 'next/link';
import { loadPostsByTopic } from './posts';
export default async function BlogPage({
searchParams,
}: {
searchParams: Promise<{ topic?: string }>;
}) {
const { topic } = await searchParams;
const posts = await loadPostsByTopic(topic);
return (
<main>
<h1>{topic ? `Posts about ${topic}` : 'All posts'}</h1>
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
</main>
);
}
Here, topic changes the server-loaded list. Reading searchParams in a Server Component page opts that page into dynamic rendering because the value comes from the incoming request. This says nothing about a particular fetch cache or revalidation policy; the example’s data functions simply read an in-file array.
There is a different choice when the full list has already been loaded and the query only controls filtering in the browser. A Client Component can use useSearchParams for that job. For example, this could be used instead of the server-side topic filter above, with posts supplied by its parent:
// app/blog/ClientPostFilter.tsx
'use client';
import { useSearchParams } from 'next/navigation';
type PostPreview = { slug: string; title: string; topic: string };
export default function ClientPostFilter({ posts }: { posts: PostPreview[] }) {
const topic = useSearchParams().get('topic');
const visible = topic
? posts.filter((post) => post.topic === topic)
: posts;
return <ul>{visible.map((post) => <li key={post.slug}>{post.title}</li>)}</ul>;
}
That distinction—searchParams for loading page data, useSearchParams for client-only use of query values—is the practical rule in the layouts and pages documentation.
Pre-render known post paths
The post page reads its identity from params, not from the index page’s topic query. generateStaticParams supplies the known slug values for build-time prerendering:
// app/blog/[slug]/page.tsx
import { loadAllPosts, loadPostBySlug } from '../posts';
export async function generateStaticParams() {
const posts = await loadAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await loadPostBySlug(slug);
return (
<main>
{post ? <h1>{post.title}</h1> : <p>Post not found.</p>}
</main>
);
}
The returned objects correspond to dynamic path segments, such as /blog/first-post. They are not query-string combinations: generateStaticParams describes post paths, while /blog?topic=next is a query-driven view of the index. The linking and navigating documentation describes generateStaticParams as the way to ensure eligible dynamic paths are generated at build time.
Give the blog a loading fallback
A loading.tsx in the blog folder provides UI while that route content is loading. Next.js places the page content inside a Suspense boundary and can show the fallback until the content is ready:
// app/blog/loading.tsx
export default function Loading() {
return <p>Loading blog content…</p>;
}
To check the hierarchy, follow the links from /blog to a post and back, then try the topic link. With <Link> navigation, the shared header and blog navigation remain while the page content changes; if the next content is still loading, the loading fallback can take its place temporarily. That is the route structure doing its job, not a second copy of the layout.




