Fetching Data with Server Components
In the App Router, page.tsx and most components are Server Components by default. They can be async: await a database or fetch, then return JSX. The browser never sees your query — only the finished HTML (plus the RSC payload).
async Server Components — fetch where the HTML is built
In the App Router, page.tsx and most components are Server Components by default. They can be async: await a database or fetch, then return JSX. The browser never sees your query — only the finished HTML (plus the RSC payload).
Real-life example: A Server Component is a chef who cooks in the kitchen and brings a plated dish to the table. Guests do not walk into the kitchen and grab raw ingredients.
import { sql } from "@vercel/postgres";
async function getDashboardStats() {
const { rows } = await sql`
SELECT
COUNT(*)::int AS total,
COALESCE(SUM(amount), 0)::int AS revenue
FROM invoices
WHERE status = 'paid'
`;
return rows[0];
}
export default async function DashboardPage() {
const stats = await getDashboardStats();
return (
<main>
<h1>Dashboard</h1>
<p>Paid invoices: {stats.total}</p>
<p>Revenue: ₹{stats.revenue}</p>
</main>
);
}fetch in Next.js — caching notes
Next.js extends fetch. By default (depending on version and route settings), results can be cached and reused across requests. That is great for a blog, wrong for a live bank balance.
You control the behavior with options and route segment config.
- fetch(url, { cache: 'force-cache' }) — prefer cached (static-friendly)
- fetch(url, { cache: 'no-store' }) — always fresh (dynamic)
- fetch(url, { next: { revalidate: 60 } }) — ISR-style: refresh at most every 60s
- fetch(url, { next: { tags: ['invoices'] } }) — tag for on-demand revalidation
// Good for a public course catalog (can be slightly stale)
const courses = await fetch("https://api.example.com/courses", {
next: { revalidate: 3600, tags: ["courses"] },
}).then((r) => r.json());
// Good for the signed-in user's notifications
const notes = await fetch("https://api.example.com/notifications", {
cache: "no-store",
headers: { Authorization: `Bearer ${token}` },
}).then((r) => r.json());Dashboard data example — parallel awaits
If two queries do not depend on each other, start them together with Promise.all so the page waits once, not twice in a row.
Real-life example: Asking two assistants to get coffee and printouts at the same time beats sending one, waiting, then sending the other.
async function getRecentInvoices() {
const res = await fetch("https://api.example.com/invoices?limit=5", {
next: { tags: ["invoices"] },
});
return res.json();
}
async function getCustomers() {
const res = await fetch("https://api.example.com/customers", {
next: { revalidate: 300 },
});
return res.json();
}
export default async function DashboardPage() {
const [invoices, customers] = await Promise.all([
getRecentInvoices(),
getCustomers(),
]);
return (
<section>
<h2>Recent invoices ({invoices.length})</h2>
<p>Customers on file: {customers.length}</p>
</section>
);
}