FroquizFroquiz
HomeQuizzesSenior ChallengeGet CertifiedBlogAbout
Sign InStart Quiz
Sign InStart Quiz
Froquiz

The most comprehensive quiz platform for software engineers. Test yourself with 10000+ questions and advance your career.

LinkedIn

Platform

  • Start Quizzes
  • Topics
  • Blog
  • My Profile
  • Sign In

About

  • About Us
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

Β© 2026 Froquiz. All rights reserved.Built with passion for technology
Blog & Articles

Next.js App Router: Server Components, Data Fetching, Caching and Best Practices

Master the Next.js App Router. Covers Server vs Client Components, layouts, data fetching patterns, caching strategies, Server Actions, streaming, and migration from Pages Router.

Yusuf SeyitoğluMarch 17, 20261 views11 min read

Next.js App Router: Server Components, Data Fetching, Caching and Best Practices

The App Router, introduced in Next.js 13 and stabilized in Next.js 14, represents the biggest paradigm shift in React development in years. React Server Components change where code runs, how data is fetched, and what ships to the browser. Understanding the App Router deeply is now a prerequisite for senior frontend roles at most companies.

The Core Shift: Server vs Client Components

In the App Router, every component is a Server Component by default. Server Components run exclusively on the server β€” they never ship to the browser.

tsx
-- app/users/page.tsx -- This is a Server Component by default -- It can be async, access databases, read environment variables async function UsersPage() { -- Direct database access -- no API route needed const users = await db.users.findMany({ take: 20 }); return ( <main> <h1>Users</h1> {users.map(user => ( <UserCard key={user.id} user={user} /> ))} </main> ); } export default UsersPage;

Server Components:

  • Run on the server, never on the client
  • Can directly access databases, file systems, secrets
  • Cannot use hooks (useState, useEffect) or browser APIs
  • Their code never ships to the JavaScript bundle β€” smaller bundles

Client Components are opted in with "use client":

tsx
"use client"; import { useState } from "react"; export function SearchBar({ onSearch }: { onSearch: (q: string) => void }) { const [query, setQuery] = useState(""); return ( <input value={query} onChange={e => setQuery(e.target.value)} onKeyDown={e => e.key === "Enter" && onSearch(query)} placeholder="Search..." /> ); }

Client Components:

  • Run on the server (for initial HTML) AND the client
  • Can use React hooks and browser APIs
  • Their code IS included in the JavaScript bundle

File System Routing

code
app/ β”œβ”€β”€ layout.tsx -- root layout (wraps everything) β”œβ”€β”€ page.tsx -- / β”œβ”€β”€ loading.tsx -- automatic Suspense boundary β”œβ”€β”€ error.tsx -- error boundary β”œβ”€β”€ not-found.tsx -- 404 page β”œβ”€β”€ blog/ β”‚ β”œβ”€β”€ layout.tsx -- layout for /blog/* β”‚ β”œβ”€β”€ page.tsx -- /blog β”‚ └── [slug]/ β”‚ └── page.tsx -- /blog/my-post β”œβ”€β”€ dashboard/ β”‚ β”œβ”€β”€ (overview)/ -- route group (no URL segment) β”‚ β”‚ └── page.tsx -- /dashboard β”‚ └── settings/ β”‚ └── page.tsx -- /dashboard/settings └── api/ └── webhooks/ └── route.ts -- /api/webhooks (Route Handler)

Layouts and Templates

tsx
-- app/layout.tsx (root layout -- required) export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <Header /> <main>{children}</main> <Footer /> </body> </html> ); } -- app/dashboard/layout.tsx (nested layout -- persists across navigation) export default function DashboardLayout({ children }: { children: React.ReactNode }) { return ( <div className="dashboard"> <Sidebar /> <div className="content">{children}</div> </div> ); }

Layouts persist across navigation β€” Sidebar does not remount when navigating between /dashboard and /dashboard/settings.

Data Fetching

Async Server Components

tsx
-- app/posts/[slug]/page.tsx interface Props { params: { slug: string }; } async function PostPage({ params }: Props) { const post = await fetchPost(params.slug); if (!post) notFound(); -- renders not-found.tsx return ( <article> <h1>{post.title}</h1> <p>{post.content}</p> </article> ); } -- Generate static params for static generation export async function generateStaticParams() { const posts = await fetchAllPosts(); return posts.map(post => ({ slug: post.slug })); } export default PostPage;

Parallel data fetching

tsx
async function Dashboard() { -- Fetch in parallel -- not sequentially const [user, posts, analytics] = await Promise.all([ fetchUser(), fetchPosts(), fetchAnalytics(), ]); return ( <div> <UserCard user={user} /> <PostList posts={posts} /> <AnalyticsChart data={analytics} /> </div> ); }

Streaming with Suspense

tsx
import { Suspense } from "react"; async function SlowComponent() { const data = await fetchSlowData(); -- takes 2 seconds return <DataDisplay data={data} />; } export default function Page() { return ( <div> <FastHeader /> -- renders immediately <Suspense fallback={<Skeleton />}> <SlowComponent /> -- streams in when ready </Suspense> </div> ); }

The page sends HTML immediately. The slow component streams in without blocking the fast content.

Caching in the App Router

Next.js has four caching layers:

Request Memoization

Within a single render, identical fetch calls are deduplicated automatically:

tsx
-- Both components call fetchUser(1) -- only ONE network request is made async function Avatar() { const user = await fetchUser(1); -- fetched once return <img src={user.avatar} />; } async function Username() { const user = await fetchUser(1); -- returns memoized result return <span>{user.name}</span>; }

Data Cache

fetch responses are cached across requests until explicitly revalidated:

tsx
-- Cache forever (static) const data = await fetch(url, { cache: "force-cache" }); -- Cache for 60 seconds (ISR-style) const data = await fetch(url, { next: { revalidate: 60 } }); -- Never cache (dynamic) const data = await fetch(url, { cache: "no-store" });

Route Segment Config

tsx
-- Page-level cache control export const revalidate = 3600; -- revalidate every hour export const dynamic = "force-dynamic"; -- always dynamic export const dynamic = "force-static"; -- always static

On-demand revalidation

tsx
-- Invalidate cache tags when data changes import { revalidateTag } from "next/cache"; export async function updatePost(id: string, data: PostData) { await db.posts.update(id, data); revalidateTag(`post-${id}`); -- invalidate all fetches tagged with this revalidateTag("posts"); -- invalidate post list caches } -- In your fetch call, add the tag const post = await fetch(`/api/posts/${id}`, { next: { tags: [`post-${id}`, "posts"] } });

Server Actions

Server Actions let you call server-side code directly from components β€” no API route needed:

tsx
-- app/posts/new/page.tsx import { redirect } from "next/navigation"; async function createPost(formData: FormData) { "use server"; -- marks this as a Server Action const title = formData.get("title") as string; const content = formData.get("content") as string; if (!title || title.length < 5) { throw new Error("Title too short"); } const post = await db.posts.create({ title, content }); revalidateTag("posts"); redirect(`/posts/${post.slug}`); } export default function NewPostPage() { return ( <form action={createPost}> <input name="title" required /> <textarea name="content" required /> <button type="submit">Publish</button> </form> ); }

Server Actions with useActionState (React 19)

tsx
"use client"; import { useActionState } from "react"; async function loginAction(prevState: any, formData: FormData) { "use server"; const email = formData.get("email") as string; const password = formData.get("password") as string; const result = await authenticate(email, password); if (!result.success) return { error: result.message }; redirect("/dashboard"); } export function LoginForm() { const [state, action, isPending] = useActionState(loginAction, null); return ( <form action={action}> <input name="email" type="email" /> <input name="password" type="password" /> {state?.error && <p className="error">{state.error}</p>} <button type="submit" disabled={isPending}> {isPending ? "Signing in..." : "Sign in"} </button> </form> ); }

Metadata API

tsx
-- Static metadata export const metadata = { title: "My Blog", description: "Articles about software development", }; -- Dynamic metadata export async function generateMetadata({ params }: Props) { const post = await fetchPost(params.slug); return { title: post.title, description: post.summary, openGraph: { title: post.title, description: post.summary, images: [{ url: post.coverImage }], }, }; }

Common Interview Questions

Q: What is the difference between a Server Component and a Client Component?

Server Components run only on the server β€” they have zero JavaScript bundle cost, can access backend resources directly, and cannot use hooks or browser APIs. Client Components run on both server (for SSR) and client β€” they can use hooks, event handlers, and browser APIs but their code ships to the browser. Default in App Router is Server Component; add "use client" to opt into Client Component.

Q: How does streaming improve perceived performance?

Without streaming, the server waits until all data is fetched before sending HTML. With streaming (via Suspense), Next.js sends the initial HTML shell immediately, then streams HTML chunks as each Suspense boundary resolves. Users see content faster β€” the fast parts appear right away while slow parts load progressively.

Q: When would you use a Server Action vs a Route Handler?

Server Actions are ideal for form submissions and mutations triggered from components β€” less boilerplate, no manual fetch call, automatic CSRF protection. Route Handlers (route.ts) are better for webhooks, third-party integrations, or when you need more control over request/response headers. Both run on the server.

Practice on Froquiz

Next.js and React Server Components are increasingly tested at frontend and full-stack interviews. Test your React knowledge on Froquiz β€” covering hooks, rendering, and performance.

Summary

  • Every component is a Server Component by default β€” add "use client" only when needed
  • Server Components can be async, access databases, and never ship JS to the browser
  • Layouts persist across navigation; loading.tsx and error.tsx are automatic Suspense/error boundaries
  • Promise.all for parallel data fetching; Suspense for streaming slow components
  • Four caching layers: request memoization, Data Cache, Full Route Cache, Router Cache
  • Server Actions replace API routes for mutations β€” "use server" in an async function
  • revalidateTag and revalidatePath invalidate caches on demand after mutations

About Author

Yusuf Seyitoğlu

Author β†’

Other Posts

  • TanStack Query (React Query): Server State Management Done RightMar 17
  • Python Testing with pytest: Fixtures, Parametrize, Mocking and Best PracticesMar 17
  • PostgreSQL Full-Text Search: tsvector, tsquery, Ranking and Multilingual SearchMar 17
All Blogs