The Why: API Design Decisions Explained

"We didn't provide enough control over local database access that wasn't using fetch(). We had unstable_cache(), but it wasn't ergonomic." — Vercel, "Our Journey with Caching"

Understanding why Next.js's API is the way it is—and why it changed—is the difference between memorizing syntax and actually understanding your framework.

File-Based Routing: Simplicity as Philosophy

The Decision

In most frameworks, you define routes in configuration files. In Next.js, routes are defined by the file system:

pages/
  index.js      → /
  about.js      → /about
  blog/[id].js  → /blog/:id

Why This Design?

  1. Zero configuration — No router setup, no route registration
  2. Discoverability — Want to know what routes exist? Look at the files
  3. Automatic code splitting — Each page is a separate bundle
  4. Convention over configuration — The file structure IS the router

The Trade-off

"While convenient, the rigid file-system-based routing can feel restrictive for applications with complex or dynamic routing needs."

The App Router made this more complex with additional file conventions:


The Data Fetching Evolution

Pages Router Era (2019-2022)

Next.js 9.3 introduced three functions that defined how developers thought about data:

// Static Generation with data
export async function getStaticProps() {
  const data = await fetchData();
  return { props: { data } };
}

// Server-Side Rendering
export async function getServerSideProps(context) {
  const data = await fetchData();
  return { props: { data } };
}

// Dynamic routes for static generation
export async function getStaticPaths() {
  return { paths: [...], fallback: false };
}

Why This Design?

These functions made the rendering strategy explicit:

The Problems That Emerged

  1. Mutual exclusivity: getStaticProps and getServerSideProps couldn't be used together on the same page. What if you wanted static content with some dynamic data?

  2. Page-level only: These functions could only be used at the page level, not in components. Data fetching was coupled to routing.

  3. Blocking navigation: "It's obvious that user should not wait a few seconds in which nothing happens (because getServerSideProps is not finished loading) when he clicks a link."

  4. No support in _app.js: Using getInitialProps in App disabled Automatic Static Optimization.

App Router Era (2022+)

The App Router replaced these explicit functions with implicit behavior:

// In App Router, this is a Server Component by default
async function Page() {
  const data = await fetch('https://api.example.com/data');
  return <div>{data}</div>;
}

Why This Change?

  1. Components can fetch their own data — No more prop-drilling from page-level functions
  2. Streaming and Suspense — Parts of the page can load independently
  3. Server Components — Some components never ship to the client
  4. Simpler mental model — Just use async/await

The New Trade-offs

The get prefix was replaced with a more descriptive generate:

"Using the name generateStaticParams is more appropriate than getStaticPaths for the new model in the app directory."


The Caching Controversy

The V14 Decision

Next.js 14 made aggressive caching the default:

// This fetch is cached by default in v14
const data = await fetch('https://api.example.com/data');

Why They Did It

The philosophy was performance-first:

Why Developers Hated It

"Next.js invisibly overrides the fetch() call with their own caching version that's on by default and causes stale state in unexpected places."

Problems included:

  1. Invisible behavior — Developers didn't know their data was being cached
  2. Third-party library issues — Libraries using fetch internally got cached unexpectedly
  3. Debugging nightmares — Stale data with no obvious cause
  4. No easy global disable — Had to opt-out per-fetch

The V15 Reversal

Vercel heard the feedback:

"Developers on Reddit have mixed opinions on this, with one calling it 'by far my favourite change' and another describing it as 'horrible.'"

Next.js 15 made caching opt-in:

// No caching by default in v15+
const data = await fetch('https://api.example.com/data');

// Explicit caching
const data = await fetch('https://api.example.com/data', { cache: 'force-cache' });

The use cache Future

Next.js 16 introduced a new explicit caching model:

'use cache';

async function CachedComponent() {
  const data = await fetchData();
  return <div>{data}</div>;
}

"Nothing is cached by default. No more hidden caches."


The Middleware Design

The Original Vision

Middleware runs code before a request is completed—perfect for authentication, redirects, A/B testing.

// middleware.js
export function middleware(request) {
  if (!request.cookies.get('session')) {
    return Response.redirect('/login');
  }
}

The Edge Runtime Decision

Middleware was designed to run on the Edge Runtime—a lightweight environment close to users.

Why This Caused Problems

The Edge Runtime doesn't support all Node.js APIs:

Developers were "blocked even experimenting with Next 13 app directory until middleware can run as non-edge."

"What's especially frustrating is that this is self-hosted anyway. Our middleware is running in Node regardless!"

The Security Incident

In early 2025, a critical vulnerability (CVE-2025-29927) was discovered. Attackers could bypass Middleware-based authentication using a specially crafted header.

Vercel's new recommendation: "Middleware should be used only for light interception—not as your main guard."

The V15.5 Solution

Next.js 15.5 introduced stable Node.js runtime support for Middleware:

export const config = {
  runtime: 'nodejs', // Now supported
};

Image Component Design

The Problem

Images are typically the largest assets on a page. Without optimization:

The Solution

Next.js 10 introduced next/image:

import Image from 'next/image';

<Image src="/photo.jpg" width={500} height={300} alt="Photo" />

Design Decisions

  1. Automatic optimization — Images compressed on-demand
  2. Format detection — WebP/AVIF served to supporting browsers
  3. Lazy loading — Built-in by default
  4. Size prevention — Required dimensions prevent layout shift

The Trade-off

The required width and height props confuse newcomers. Why can't the component just figure it out?

The answer: preventing layout shift requires knowing dimensions before the image loads. This is a deliberate trade-off—developer inconvenience for better user experience.


Key API Design Principles

Looking across these decisions, patterns emerge:

1. Performance First

Every API exists because it makes the web faster for users. Developer convenience is secondary.

2. Convention Over Configuration

When possible, eliminate configuration. File structure is the router. Default behaviors are optimized.

3. Progressive Enhancement

Start simple, add complexity only when needed. The basic case should work without configuration.

4. Listen to Feedback

The caching reversal shows Vercel will change course when the community pushes back strongly enough.

5. Web Standards Where Possible

Use fetch(), not custom methods. Use Response, not custom objects. The closer to the platform, the more portable the code.


Key Takeaways


Sources