jsondecode.com logo

JSON in Next.js — Fetch, Parse, and Type-Safe API Data

Next.js App Router gives you two distinct environments for fetching JSON: async Server Components (run on the server, no useEffect needed) and Client Components (run in the browser, use useState/useEffect or SWR). Choosing the right pattern keeps your app fast and type-safe.

Fetching JSON in Server Components

In the App Router, any async Server Component can call fetch() directly. The result is rendered on the server and streamed to the browser — no client JavaScript required.

// app/users/page.tsx
interface User {
  id: number;
  name: string;
  email: string;
}

export default async function UsersPage() {
  const res = await fetch("https://api.example.com/users", {
    next: { revalidate: 60 }, // ISR: revalidate every 60 s
  });

  if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);

  const users: User[] = await res.json();

  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

Fetching JSON in Client Components

When you need interactivity or browser APIs, mark the component 'use client' and fetch with useEffect, or use SWR for automatic caching and revalidation.

"use client";
import { useEffect, useState } from "react";

interface Post { id: number; title: string; }

export default function PostList() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch("/api/posts")
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json() as Promise<Post[]>;
      })
      .then(setPosts)
      .catch((e) => setError(e.message));
  }, []);

  if (error) return <p>Error: {error}</p>;
  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}

// SWR alternative (npm install swr):
// const { data, error } = useSWR<Post[]>("/api/posts", (url) =>
//   fetch(url).then((r) => r.json())
// );

Type-safe JSON with Zod validation

Type assertions don't validate at runtime. Use Zod to parse the API response and get a fully-typed, validated object — any unexpected shape throws a ZodError you can catch and handle.

import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  role: z.enum(["admin", "user"]).optional(),
});

type User = z.infer<typeof UserSchema>;

async function getUser(id: number): Promise<User> {
  const res = await fetch(`https://api.example.com/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const raw = await res.json();
  return UserSchema.parse(raw); // throws ZodError if invalid
}

Handling API route JSON responses

Next.js App Router API routes return a NextResponse. Use NextResponse.json() to serialise a value with the correct Content-Type header.

// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const users = [{ id: 1, name: "Alice" }];
  return NextResponse.json(users);
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  // body is typed as any — validate with Zod before use
  const newUser = { id: 2, name: body.name };
  return NextResponse.json(newUser, { status: 201 });
}

Caching and revalidation with fetch options

Next.js extends the native fetch API with caching options. Pass cache or next.revalidate to control how long the response is cached at the request level.

// Always fresh — opt out of caching (dynamic)
const res = await fetch(url, { cache: "no-store" });

// Cache indefinitely (static — like getStaticProps)
const res = await fetch(url, { cache: "force-cache" });

// Incremental Static Regeneration — revalidate every N seconds
const res = await fetch(url, { next: { revalidate: 3600 } });

// Tag-based revalidation — invalidate via revalidateTag()
const res = await fetch(url, { next: { tags: ["users"] } });

Frequently Asked Questions

How do I fetch JSON in a Next.js Server Component?

Make the component async and call fetch() directly at the top level. Use the next.revalidate option for ISR or cache: 'no-store' for dynamic data. No useEffect or useState needed.

How do I type JSON API responses in Next.js?

Cast the result of res.json() with a TypeScript interface: const data = await res.json() as MyType. For runtime safety, use Zod: define a schema and call schema.parse(await res.json()).

What is the difference between cache: 'no-store' and next.revalidate in Next.js?

cache: 'no-store' disables caching entirely — the request runs on every page render. next.revalidate: N caches the response and regenerates it in the background after N seconds (Incremental Static Regeneration).

How do I return JSON from a Next.js API route?

In the App Router, use NextResponse.json(data) in a route.ts file. It sets the Content-Type: application/json header automatically. Pass a second argument for options like { status: 201 }.

Should I fetch JSON in a Server Component or a Client Component?

Prefer Server Components for initial data fetching — they avoid client-side waterfalls and reduce JavaScript bundle size. Use Client Components (with useEffect or SWR) only when you need user interactions, browser APIs, or real-time updates.

Format and validate your JSON instantly

Free, no ads, no sign-up. Also converts JSON to TypeScript, YAML, CSV, and more.

Open JSON Formatter →

If jsondecode.com saved you time, share it with your team

Free forever. No ads. No sign-up. Help other developers find it.