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:
| Feature | Pages Router | App Router |
|---|---|---|
| Data fetching | getServerSideProps, getStaticProps | async/await directly in the component |
| Mutations | API routes + fetch | Server Actions ("use server") |
| Layouts | Custom _app.tsx wrapping | Nested layout.tsx files |
| Streaming | Not built-in | Built-in with Suspense |
| Default rendering | Client (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
| Feature | Next.js 15 | Remix |
|---|---|---|
| Routing | File-system (app/) | File-system (app/routes/) |
| Data loading | Server Components + async/await | loader functions |
| Mutations | Server Actions | action functions |
| Streaming | Built-in Suspense | Built-in defer |
| Auth library | NextAuth v5 | Remix-Auth |
| Deployment | Vercel (optimal), any Node host | Fly.io, any Node/edge host |
| Edge runtime | Supported | Supported |
| Learning curve | Moderate (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.