In Next.js 16 the simplest, fastest, most-cacheable way to load data is to call fetch (or your DB client) directly inside a async Server Component.
1// app/courses/page.tsx
2export default async function CoursesPage() {
3 const res = await fetch("https://api.coachnest.dev/courses");
4 const courses: Course[] = await res.json();
5
6 return (
7 <ul>
8 {courses.map((c) => (
9 <li key={c.id}>{c.title}</li>
10 ))}
11 </ul>
12 );
13}No useEffect. No loading flicker on the client. The HTML arrives populated.
1import { db } from "@/lib/db";
2
3export default async function CoursesPage() {
4 const courses = await db.course.findMany({
5 where: { status: "PUBLISHED" },
6 select: { id: true, title: true, slug: true },
7 orderBy: { createdAt: "desc" },
8 });
9
10 return courses.map((c) => <CourseCard key={c.id} course={c} />);
11}Avoid waterfalls — request things in parallel.
1// ❌ Serial — 2 round trips back-to-back
2const user = await getUser(id);
3const orders = await getOrders(id);
4
5// ✅ Parallel — both fire at the same time
6const [user, orders] = await Promise.all([getUser(id), getOrders(id)]);Suspense + multiple Server Components let you stream independent regions:
1export default function Dashboard() {
2 return (
3 <>
4 <Suspense fallback={<EarningsSkeleton />}>
5 <Earnings /> {/* its own async server component, own DB call */}
6 </Suspense>
7 <Suspense fallback={<StudentsSkeleton />}>
8 <Students />
9 </Suspense>
10 </>
11 );
12}The browser sees one or the other render as data arrives — TTFB is dictated by the fastest fetch, not the slowest.
Multiple components in the same render that call fetch() with the same URL & options share a single network request:
1async function getMe() {
2 const res = await fetch("/api/me");
3 return res.json();
4}
5
6// Both call getMe() in the same render — only ONE network request fires.For non-fetch deduplication, wrap helpers with React's cache():
1import { cache } from "react";
2
3export const getUser = cache(async (id: string) => {
4 return db.user.findUnique({ where: { id } });
5});1import { cookies, headers } from "next/headers";
2
3const session = (await cookies()).get("session")?.value;
4const ua = (await headers()).get("user-agent");In Next.js 16 cookies() and headers() are asynchronous — always await them.
1const res = await fetch("https://api.example.com/v1/users", {
2 headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
3});The token never reaches the client. The fetch happens server-side, full stop.
1const res = await fetch("/api/something");
2
3if (!res.ok) {
4 if (res.status === 404) notFound();
5 throw new Error(`API failed: ${res.status}`);
6}
7
8const data = await res.json();notFound() and redirect() are special — they don't throw user-visible errors, they unwind to the routing layer.