ai next tutorial 2026-08-30

By Xah Lee. Date: .
work in progress

write a tutorial of next js

ai answer

Next.js Tutorial (App Router)

This is a practical introduction to Next.js 16 (current stable: 16.3.3). You will learn how to create an app, route pages, mix Server and Client Components, fetch data, handle forms, and deploy.

Official docs: https://nextjs.org/docs and https://nextjs.org/learn .


1. What Next.js is

Next.js is a React framework for full-stack web apps. On top of React it gives you:

There are two routers:

Router Status
App Router (app/) Recommended. Server Components, layouts, streaming
Pages Router (pages/) Still supported for older apps

This tutorial uses the App Router only.


2. Prerequisites

Check Node:

node -v

3. Create a project

Recommended (pnpm, npm, or yarn all work):

npx create-next-app@latest my-app

Or skip prompts:

pnpm create next-app my-app --yes
cd my-app
pnpm dev

Open http://localhost:3000.

Recommended defaults today: TypeScript, ESLint, Tailwind CSS, App Router, import alias @/*.

Useful scripts:

{
  "dev": "next dev",
  "build": "next build",
  "start": "next start"
}

Turbopack is the default for development. Use next dev --webpack only if you need Webpack.


4. Project structure

A typical new app looks like this:

my-app/
├── app/
│   ├── layout.tsx      # root layout (required)
│   ├── page.tsx        # home page → /
│   ├── globals.css
│   └── favicon.ico
├── public/             # static files → /filename
├── next.config.ts
├── package.json
└── tsconfig.json

Special files inside app/:

File Role
page.tsx Makes the folder a public route
layout.tsx Shared UI that wraps pages
loading.tsx Instant loading UI (Suspense)
error.tsx Error boundary for that segment
not-found.tsx Custom 404
route.ts API / Route Handler
template.tsx Like a layout, but remounts on navigation

A folder is not a URL unless it contains page.tsx or route.ts. You can colocate components next to pages safely.


5. Pages and layouts

A page

app/page.tsx is /:

export default function Page() {
  return <h1>Home</h1>
}

Nested pages

app/about/page.tsx          → /about
app/blog/page.tsx           → /blog
app/blog/[slug]/page.tsx    → /blog/hello-world

Root layout (required)

Every app needs a root layout with <html> and <body>:

// app/layout.tsx
import type { Metadata } from 'next'
import './globals.css'

export const metadata: Metadata = {
  title: 'My App',
  description: 'A Next.js tutorial app',
}

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

Layouts do not remount on navigation. State in a shared header survives route changes.

Nested layout

// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <section>
      <aside>Blog sidebar</aside>
      {children}
    </section>
  )
}

/blog and /blog/[slug] both wrap in this layout and the root layout.


6. Routing conventions

app/blog/[slug]/page.tsx           # /blog/my-post
app/shop/[...slug]/page.tsx        # /shop/a, /shop/a/b
app/docs/[[...slug]]/page.tsx      # /docs and /docs/a/b
app/(marketing)/about/page.tsx     # /about  (group does not appear in URL)
app/blog/_components/Card.tsx      # private folder, not a route

Dynamic params are Promises — await them:

// app/blog/[slug]/page.tsx
export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  return <h1>Post: {slug}</h1>
}

searchParams works the same way (Promise<{ q?: string }>).


7. Linking and navigation

Use <Link> for internal routes. Next.js prefetches linked pages in the viewport.

import Link from 'next/link'

export default function Nav() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
      <Link href="/blog/hello">Hello post</Link>
    </nav>
  )
}

Client-side navigation (after an event):

'use client'

import { useRouter } from 'next/navigation'

export function BackButton() {
  const router = useRouter()
  return <button onClick={() => router.push('/blog')}>Back</button>
}

8. Server vs Client Components

Default: Server Components. They run on the server, can await data and use secrets, and ship no extra client JS for themselves.

Use a Client Component when you need:

Mark the entry file with 'use client' at the very top:

// app/components/counter.tsx
'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount((c) => c + 1)}>
      Clicked {count} times
    </button>
  )
}

Compose them: fetch on the server, pass props into a small client island.

// app/blog/[slug]/page.tsx  (Server Component)
import LikeButton from '@/app/components/like-button'
import { getPost } from '@/lib/posts'

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
      <LikeButton initialLikes={post.likes} />
    </article>
  )
}

Rule of thumb: keep 'use client' as low in the tree as possible.


9. Fetching data

In a Server Component you can async/await directly:

export default async function BlogPage() {
  const res = await fetch('https://api.vercel.app/blog')
  const posts = await res.json()

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

Database access is also fine here — credentials stay on the server.

Parallel fetches (faster):

const [user, posts] = await Promise.all([getUser(), getPosts()])

Share a fetch within one request with cache():

import { cache } from 'react'

export const getUser = cache(async () => {
  const res = await fetch('https://api.example.com/user')
  return res.json()
})

Streaming

Show the shell immediately, stream the slow part:

import { Suspense } from 'react'

export default function Page() {
  return (
    <>
      <h1>Blog</h1>
      <Suspense fallback={<p>Loading posts…</p>}>
        <PostList />
      </Suspense>
    </>
  )
}

Or add app/blog/loading.tsx for a segment-wide skeleton.


10. Server Actions (mutations)

Server Actions let forms talk to the server without writing a separate API route.

// app/actions.ts
'use server'

import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  // save to DB…
  revalidatePath('/blog')
  redirect('/blog')
}
// app/blog/new/page.tsx
import { createPost } from '@/app/actions'

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <button type="submit">Publish</button>
    </form>
  )
}

redirect() after a successful mutation; revalidatePath() / revalidateTag() to refresh cached UI.


11. Route Handlers (APIs)

app/api/hello/route.tsGET /api/hello

export async function GET() {
  return Response.json({ message: 'Hello from Next.js' })
}

export async function POST(request: Request) {
  const body = await request.json()
  return Response.json({ received: body }, { status: 201 })
}

Use Route Handlers for webhooks, public JSON APIs, and non-form clients. Prefer Server Actions for UI forms in the same app.


12. Images, fonts, metadata

Images

import Image from 'next/image'

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority
/>

Remote images need images.remotePatterns in next.config.ts.

Fonts

import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

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

SEO metadata

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: {
    default: 'My Blog',
    template: '%s | My Blog',
  },
  description: 'Notes on Next.js',
}

Per-page (dynamic):

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>
}): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)
  return { title: post.title, description: post.excerpt }
}

13. Styling

New apps include Tailwind CSS. Utility classes work immediately:

<h1 className="text-3xl font-bold tracking-tight">Hello</h1>

You can also use CSS Modules (Button.module.css) or global CSS from app/globals.css.


14. Errors and not-found

// app/blog/error.tsx
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div>
      <p>Something went wrong.</p>
      <button onClick={reset}>Try again</button>
    </div>
  )
}
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)
  if (!post) notFound()
  return <h1>{post.title}</h1>
}

Add app/not-found.tsx for a custom 404 page.


15. Environment variables

# .env.local
DATABASE_URL=postgres://…
NEXT_PUBLIC_SITE_URL=http://localhost:3000

Never put API secrets in Client Components.


16. Build and deploy

pnpm build    # production build
pnpm start    # run the production server

Vercel is the path of least resistance: push to GitHub, import the repo, done. Next.js also runs on Node hosts, Docker, and platforms that support the Build Adapters API.

Before production: use Server Components by default, <Link> for navigation, custom error.tsx / not-found.tsx, and keep client JS small.

Stay on a patched release (as of late August 2026 that is 16.3.3).


Mini project: put it together

  1. Create the app with create-next-app.
  2. Add app/blog/page.tsx — list posts from fetch.
  3. Add app/blog/[slug]/page.tsx — await params, fetch one post, call notFound() if missing.
  4. Add app/blog/layout.tsx — shared blog chrome.
  5. Add a Client LikeButton.
  6. Add app/blog/new/page.tsx + a Server Action to create a post.
  7. Add generateMetadata on the post page.
  8. Deploy.

Common mistakes


Where to go next

If you want a follow-up, say which direction you prefer: a blog with a database, auth, or a dashboard with Server Actions.