DEV Community

Alex Spinov
Alex Spinov

Posted on

Next.js 15 Has a Free API — Everything New in the React Framework

Next.js 15 is the most popular React framework — now with Turbopack stable, React 19 support, partial prerendering, and the new after() API. Here's everything you get for free.

What's New in Next.js 15

  • Turbopack stable — 76% faster local dev, 96% faster HMR
  • React 19 support — Server Components, Actions, use() hook
  • Partial Prerendering (PPR) — static shell + dynamic content
  • after() API — run code after response is sent
  • Enhanced fetch caching — no more aggressive caching surprises
  • next/form — client-side navigation for forms

Server Components (Default in App Router)

// app/users/page.tsx — this is a Server Component by default
async function UsersPage() {
  // Direct database access — no API route needed!
  const users = await db.user.findMany({
    select: { id: true, name: true, email: true },
  });

  return (
    <div>
      <h1>Users ({users.length})</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            {user.name}  {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UsersPage;
Enter fullscreen mode Exit fullscreen mode

No useEffect. No loading states. No API routes. Server Components fetch data directly.

Server Actions (Forms Without API Routes)

// app/contacts/page.tsx
async function createContact(formData: FormData) {
  "use server";

  const name = formData.get("name") as string;
  const email = formData.get("email") as string;

  await db.contact.create({ data: { name, email } });
  revalidatePath("/contacts");
}

export default async function ContactsPage() {
  const contacts = await db.contact.findMany();

  return (
    <div>
      <form action={createContact}>
        <input name="name" placeholder="Name" required />
        <input name="email" type="email" placeholder="Email" required />
        <button type="submit">Add Contact</button>
      </form>

      <ul>
        {contacts.map((c) => (
          <li key={c.id}>{c.name}</li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The after() API (New in 15!)

import { after } from "next/server";

export async function POST(request: Request) {
  const data = await request.json();

  // Process the request
  const result = await processOrder(data);

  // This runs AFTER the response is sent to the user
  after(async () => {
    await sendConfirmationEmail(data.email);
    await updateAnalytics("order_created");
    await notifySlack(`New order: ${result.id}`);
  });

  // User gets fast response, background work happens after
  return Response.json(result);
}
Enter fullscreen mode Exit fullscreen mode

Partial Prerendering (PPR)

// next.config.js
export default {
  experimental: {
    ppr: true,
  },
};

// app/dashboard/page.tsx
import { Suspense } from "react";

// Static shell (prerendered at build time)
export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>  {/* Static */}
      <nav>...</nav>       {/* Static */}

      {/* Dynamic — streams in after static shell loads */}
      <Suspense fallback={<div>Loading stats...</div>}>
        <DynamicStats />
      </Suspense>

      <Suspense fallback={<div>Loading feed...</div>}>
        <ActivityFeed />
      </Suspense>
    </div>
  );
}

// This component is dynamic — fetches fresh data each request
async function DynamicStats() {
  const stats = await getRealtimeStats(); // No cache
  return <div>Revenue: ${stats.revenue}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Static parts load instantly (from CDN). Dynamic parts stream in. Best of both worlds.

Middleware

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // Auth check
  const token = request.cookies.get("session");

  if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  // A/B testing
  const response = NextResponse.next();
  if (!request.cookies.get("variant")) {
    response.cookies.set("variant", Math.random() > 0.5 ? "A" : "B");
  }

  // Geolocation
  const country = request.geo?.country || "US";
  response.headers.set("x-country", country);

  return response;
}

export const config = {
  matcher: ["/dashboard/:path*", "/api/:path*"],
};
Enter fullscreen mode Exit fullscreen mode

Route Handlers (API Routes in App Router)

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

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const page = Number(searchParams.get("page")) || 1;

  const users = await db.user.findMany({
    skip: (page - 1) * 10,
    take: 10,
  });

  return NextResponse.json({ users, page });
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const user = await db.user.create({ data: body });
  return NextResponse.json(user, { status: 201 });
}
Enter fullscreen mode Exit fullscreen mode

Parallel Routes

app/
  @analytics/
    page.tsx
  @feed/
    page.tsx
  layout.tsx
Enter fullscreen mode Exit fullscreen mode
// app/layout.tsx
export default function Layout({
  children,
  analytics,
  feed,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  feed: React.ReactNode;
}) {
  return (
    <div className="grid grid-cols-3">
      <main className="col-span-2">{children}</main>
      <aside>
        {analytics}
        {feed}
      </aside>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Both @analytics and @feed load in parallel — no waterfalls.

Next.js 15 vs Remix vs Astro

Feature Next.js 15 Remix Astro
RSC Yes (default) No No
PPR Yes (new!) No No
Server Actions Yes Actions No
Turbopack Stable (76% faster) Vite Vite
Static export Yes Limited Yes (default)
Edge runtime Yes Yes Yes
Image optimization Built-in Manual Built-in
Deploy Anywhere (Vercel optimized) Anywhere Anywhere

Need to scrape data from any website and get it in structured JSON? Check out my web scraping tools on Apify — no coding required, results in minutes.

Have a custom data extraction project? Email me at spinov001@gmail.com — I build tailored scraping solutions for businesses.

Top comments (0)