Fetching with fetch

fetch in Server Components

Next.js uses the standard Web fetch. On the server you can add Next.js options: cache and next: { revalidate, tags }. In Next.js 15+, fetch is not cached by default; you opt in.

// lib/api.ts
export type Product = { id: number; name: string; price: number; category: string };

const API = process.env.API_URL ?? 'http://localhost:8080';

export async function getProduct(id: number): Promise<Product | null> {
  const res = await fetch(API + '/api/products/' + id, {
    next: { revalidate: 60, tags: ['product-' + id] },
  });
  if (res.status === 404) return null;
  if (!res.ok) throw new Error('Product API failed: ' + res.status);
  return (await res.json()) as Product;
}

Typing and Checking the Response

res.json() returns any. Cast it to your type, or better, check it at runtime (see the TypeScript material, Type-safe Patterns). A small guard:

Example

type Product = { id: number; name: string; price: number };

function isProduct(x: unknown): x is Product {
  if (typeof x !== 'object' || x === null) return false;
  const o = x as Record<string, unknown>;
  return typeof o.id === 'number' && typeof o.name === 'string' && typeof o.price === 'number';
}

async function main() {
  // Response exists in Node 18+, so we can build one locally (no network)
  const ok = new Response(JSON.stringify({ id: 1, name: 'Clean Code', price: 450 }));
  const bad = new Response(JSON.stringify({ id: '1', title: 'oops' }));

  for (const res of [ok, bad]) {
    const data: unknown = await res.json();
    console.log(isProduct(data) ? 'valid: ' + data.name : 'invalid payload: ' + JSON.stringify(data));
  }
}
main();

Output

valid: Clean Code
invalid payload: {"id":"1","title":"oops"}