Next.js 15 Tutorial 2026: App Router, Server Components, Server Actions, and Prisma

Next.js 15 Tutorial 2026: App Router, Server Components, Server Actions, and Prisma

Most Next.js tutorials online still teach the Pages Router. In 2026, that approach is outdated. This Next.js 15 tutorial walks you through building a full-stack application using the App Router — the architecture that ships with every new create-next-app project and unlocks React Server Components, Server Actions, and streaming by default.

By the end you will have a working app with a database, authentication, and form handling — no custom API routes required for mutations.


1. App Router vs Pages Router: Why It Matters

Before writing any code, understand the fundamental shift. The Pages Router (the pages/ directory) predates React Server Components. Every component hydrated in the browser, even components that only render static text. The result: large JavaScript bundles sent to the client for no benefit.

The App Router (the app/ directory, introduced in Next.js 13 and stable in Next.js 14/15) defaults to React Server Components. Server Components run only on the server — they never appear in your JavaScript bundle. Real-world benchmarks consistently show 40–60% bundle size reductions when migrating from Pages Router to App Router on data-heavy pages.

Practical differences at a glance:

FeaturePages RouterApp Router
Data fetchinggetServerSideProps, getStaticPropsasync/await directly in the component
MutationsAPI routes + fetchServer Actions ("use server")
LayoutsCustom _app.tsx wrappingNested layout.tsx files
StreamingNot built-inBuilt-in with Suspense
Default renderingClient (hydrated)Server Component

Stick with the App Router for any new project in 2026.


2. Create the Project

Scaffold a new Next.js 15 app with TypeScript and the App Router enabled:

npx create-next-app@latest my-app --typescript --app --eslint --tailwind --src-dir
cd my-app

The --app flag ensures the generator creates an app/ directory instead of pages/. The generated structure looks like this:

my-app/
  src/
    app/
      layout.tsx       ← root layout
      page.tsx         ← home route "/"
      globals.css
  prisma/              ← you will add this
  public/
  next.config.ts
  tsconfig.json

Run the dev server to confirm everything works:

npm run dev

Open http://localhost:3000. You should see the default Next.js welcome page.


3. App Router Fundamentals

The app/ Directory Structure

Every file named page.tsx inside app/ becomes a route. A file at app/dashboard/page.tsx maps to /dashboard. A file at app/blog/[slug]/page.tsx maps to /blog/:slug.

Server Components (Default)

Every component in the app/ directory is a React Server Component by default. Server Components can be async, they can await database queries or API calls directly, and they produce zero client-side JavaScript.

// app/posts/page.tsx  — this is a Server Component
export default async function PostsPage() {
  // fetch data directly — no useEffect, no loading state, no client bundle
  const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10", {
    next: { revalidate: 60 }, // ISR: revalidate every 60 seconds
  });
  const posts = await res.json();

  return (
    <ul>
      {posts.map((post: { id: number; title: string }) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

No getServerSideProps. No useEffect. No state management library. The data arrives with the HTML.

Client Components

When you need interactivity — event handlers, useState, useEffect, browser APIs — add "use client" as the very first line of the file:

"use client";

import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

Keep Client Components as leaf nodes in your component tree. A Server Component can import a Client Component, but a Client Component cannot import a Server Component. This boundary is enforced at build time.

Layouts

The file app/layout.tsx wraps every page in your application. Use it for persistent UI: navigation, footers, providers.

// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "My App",
  description: "Built with Next.js 15",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

Nested layouts work the same way. Create app/dashboard/layout.tsx to add a sidebar that appears on every /dashboard/* route without re-rendering on navigation.


4. Data Fetching in Server Components

Forget getServerSideProps. In the App Router you write async components:

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

export default async function UserPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params; // params is a Promise in Next.js 15
  const user: User = await fetch(`https://api.example.com/users/${id}`).then((r) => r.json());

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

Note: In Next.js 15, params and searchParams are Promises. Always await them before destructuring.

For parallel data fetching, use Promise.all:

const [user, posts] = await Promise.all([
  fetchUser(id),
  fetchUserPosts(id),
]);

5. Server Actions for Forms

Server Actions are async functions that run on the server and can be called directly from forms or Client Components. They replace the pattern of writing an API route and calling it with fetch.

Add "use server" at the top of the function (or at the top of a dedicated file):

// app/actions.ts
"use server";

import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";

export async function createPost(formData: FormData) {
  const title = formData.get("title") as string;
  const body = formData.get("body") as string;

  await db.post.create({ data: { title, body } });
  revalidatePath("/posts"); // invalidate the cached posts page
}

Use the action in a form:

// app/posts/new/page.tsx
import { createPost } from "@/app/actions";

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Post title" required />
      <textarea name="body" placeholder="Post body" required />
      <button type="submit">Create Post</button>
    </form>
  );
}

This form works even with JavaScript disabled — a property called progressive enhancement. When JS is available, React intercepts the submission for a smoother UX. When it is not, the browser submits the form natively and the Server Action still runs.


6. Prisma Setup

Prisma gives you a type-safe ORM with auto-generated TypeScript types from your database schema.

npm install prisma @prisma/client
npx prisma init

Edit prisma/schema.prisma:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  body      String
  createdAt DateTime @default(now())
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}

model User {
  id    Int    @id @default(autoincrement())
  email String @unique
  name  String
  posts Post[]
}

Add your DATABASE_URL to .env, then run migrations:

npx prisma migrate dev --name init
npx prisma generate

Create a singleton client at src/lib/db.ts:

import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

export const db =
  globalForPrisma.prisma ??
  new PrismaClient({ log: ["query"] });

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;

The singleton pattern prevents creating hundreds of database connections during hot reloads in development.


7. NextAuth v5 Authentication

NextAuth v5 (Auth.js) integrates natively with the App Router. Install it:

npm install next-auth@beta

Create src/auth.ts:

import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    GitHub({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
  ],
});

Add the route handler at app/api/auth/[...nextauth]/route.ts:

export { handlers as GET, handlers as POST } from "@/auth";

Reading the session in a Server Component requires no hook — just await auth():

import { auth } from "@/auth";

export default async function ProfilePage() {
  const session = await auth();
  if (!session) return <p>Please sign in.</p>;

  return <p>Hello, {session.user?.name}</p>;
}

Set AUTH_SECRET, GITHUB_ID, and GITHUB_SECRET in .env. Generate a secret with npx auth secret.


8. Deployment to Vercel

Vercel is the fastest path to production for a Next.js app:

npm install -g vercel
vercel --prod

The CLI detects Next.js automatically, sets up edge caching, and configures environment variables through a guided prompt. Add DATABASE_URL, AUTH_SECRET, GITHUB_ID, and GITHUB_SECRET when asked.

For the database, Vercel Postgres (powered by Neon) is a zero-config option you can provision directly from the Vercel dashboard and connect to your project in one click.


9. Next.js 15 vs Remix

FeatureNext.js 15Remix
RoutingFile-system (app/)File-system (app/routes/)
Data loadingServer Components + async/awaitloader functions
MutationsServer Actionsaction functions
StreamingBuilt-in SuspenseBuilt-in defer
Auth libraryNextAuth v5Remix-Auth
DeploymentVercel (optimal), any Node hostFly.io, any Node/edge host
Edge runtimeSupportedSupported
Learning curveModerate (RSC mental model)Moderate (web fundamentals focus)

Both are solid choices. Next.js has a larger ecosystem and is the safer choice for teams already on React. Remix shines when you want to stay close to web platform primitives.


10. FAQ

Do I need API routes at all? For mutations, no — Server Actions replace them. For third-party webhook endpoints or when you need to expose a public API, yes, use Route Handlers (app/api/.../route.ts).

Can I use Server Components with a state manager like Zustand? Server Components cannot use React context or hooks. Put your Zustand store in a Client Component provider and keep Server Components above it in the tree to avoid unnecessary hydration.

What happens if a Server Action throws an error? Uncaught errors in Server Actions propagate to the nearest error.tsx boundary. For form validation, return an object from the action and read it with the useActionState hook in a Client Component.

Is the App Router production-ready? Yes. It has been stable since Next.js 14 and is used in production by large organizations. The Pages Router is still supported but receives no new features.

How do I handle loading states? Add a loading.tsx file next to page.tsx. Next.js automatically wraps the page in a Suspense boundary and shows loading.tsx while the async component resolves — no manual Suspense required.


Summary

You now have a complete Next.js 15 App Router foundation: Server Components for zero-cost data fetching, Server Actions for form handling without custom API routes, Prisma for type-safe database access, and NextAuth v5 for authentication. The mental model shift from the Pages Router is real, but the payoff — smaller bundles, simpler data fetching, and progressive enhancement for free — makes it the right approach for every new full-stack React project in 2026.

Leonardo Lazzaro

Software engineer and technical writer. 10+ years experience in DevOps, Python, and Linux systems.

More articles by Leonardo Lazzaro