TECHNICAL PRESENTATION

Introduction to
Next.js

The Full-Stack React Framework
App Router · RSC · SSR · SSG · API routes · Vercel
02

Agenda

Foundations

  • What is Next.js & the App Router
  • App Router vs Pages Router
  • File-based routing conventions
  • Server Components vs Client Components

Data & Rendering

  • Data fetching & caching
  • Server Actions & mutations
  • SSR, SSG, ISR & streaming
  • Layouts, templates & error boundaries

Features

  • Middleware & edge functions
  • API Route Handlers
  • Styling approaches
  • Image & font optimisation

Production

  • Metadata & SEO
  • Authentication patterns
  • Database & ORM integration
  • Testing & deployment
03

What Is Next.js?

Next.js is a full-stack React framework created by Vercel (formerly ZEIT) in 2016. It extends React with server-side rendering, static generation, file-based routing, and a powerful build system — letting teams ship production-grade web applications without stitching together dozens of tools.

History

  • 2016 — First release (Pages Router)
  • 2020 — ISR, Image component
  • 2022 — App Router (v13 beta)
  • 2023 — App Router stable (v13.4)
  • 2024 — Partial Prerendering (v14/15)

Key Features

  • React Server Components
  • File-based routing
  • Server Actions (mutations)
  • Automatic code splitting
  • Built-in image/font optimisation

Who Uses It?

  • Vercel, Netflix, TikTok
  • Hulu, Notion, Twitch
  • Washington Post, Target
  • OpenAI, Stripe Dashboard
  • Most popular React framework

Next.js is NOT

A replacement for React (it uses React) • A backend-only framework (it's full-stack) • Locked to Vercel (self-hosting is fully supported)

04

App Router vs Pages Router

Next.js has two routing systems. The App Router (introduced in v13) is the recommended default. The Pages Router remains fully supported for existing projects.

FeatureApp Router (app/)Pages Router (pages/)
Server ComponentsDefault (RSC)Not supported
LayoutsNested, persistentManual with _app.tsx
Data Fetchingasync components, fetchgetServerSideProps, getStaticProps
StreamingBuilt-in with SuspenseLimited
Server ActionsNative supportNot available
Route Handlersroute.ts filespages/api/*.ts
MiddlewareSupportedSupported

Directory Structure (App Router)

app/
  layout.tsx        # Root layout
  page.tsx          # Home route (/)
  about/
    page.tsx        # /about
  blog/
    [slug]/
      page.tsx      # /blog/:slug

Migration Strategy

  • Both routers can coexist in one project
  • Migrate route-by-route incrementally
  • Move shared layouts first
  • Convert data fetching last
05

File-Based Routing

In the App Router, the file system is the router. Each folder inside app/ maps to a URL segment. Special files define behaviour at each route level.

Special Files

FilePurpose
page.tsxMakes route publicly accessible
layout.tsxShared UI for a segment
loading.tsxSuspense fallback UI
error.tsxError boundary for segment
not-found.tsx404 UI for segment
route.tsAPI endpoint (no UI)
template.tsxRe-rendered layout variant

Dynamic & Advanced Routes

# Dynamic segments
app/blog/[slug]/page.tsx     → /blog/hello-world

# Catch-all
app/docs/[...slug]/page.tsx  → /docs/a/b/c

# Optional catch-all
app/shop/[[...slug]]/page.tsx

# Route groups (no URL impact)
app/(marketing)/about/page.tsx
app/(shop)/products/page.tsx

# Parallel routes
app/@modal/login/page.tsx
app/@sidebar/page.tsx

Colocation

Only page.tsx and route.ts are publicly addressable. You can safely colocate components, tests, styles, and utilities inside route folders without exposing them as routes.

06

Server Components vs Client Components

In the App Router, components are Server Components by default. Add "use client" at the top of a file to make it a Client Component. The boundary between them is the serialisation boundary.

Server Components (default)

// app/users/page.tsx  (Server Component)
import { db } from '@/lib/db';

export default async function UsersPage() {
  const users = await db.user.findMany();

  return (
    <ul>
      {users.map(u => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}
  • Direct database/API access
  • Zero client-side JS bundle
  • Access secrets & env vars safely
  • Cannot use hooks or browser APIs

Client Components

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

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}
  • Interactivity (onClick, onChange)
  • React hooks (useState, useEffect)
  • Browser APIs (localStorage, etc.)
  • Hydrated on the client

Rule of Thumb

Keep components as Server Components until you need interactivity. Push "use client" as far down the tree as possible — wrap only the interactive leaf, not the entire page.

07

Data Fetching

In the App Router, data fetching happens inside async Server Components using the standard fetch API. Next.js extends fetch with built-in caching and revalidation.

Fetch with Caching

// Cached by default (static)
const res = await fetch('https://api.example.com/data');

// Revalidate every 60 seconds (ISR)
const res = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 }
});

// No cache (dynamic / SSR)
const res = await fetch('https://api.example.com/data', {
  cache: 'no-store'
});

generateStaticParams

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map(post => ({
    slug: post.slug,
  }));
}

export default async function BlogPost(
  { params }: { params: { slug: string } }
) {
  const post = await getPost(params.slug);
  return <article>{post.content}</article>;
}

Caching Strategies at a Glance

StrategyOptionBehaviour
Static (default)No option neededCached at build time, served from CDN
ISRnext: { revalidate: N }Stale-while-revalidate after N seconds
Dynamiccache: 'no-store'Fresh data on every request
Tag-basednext: { tags: ['posts'] }Invalidated via revalidateTag('posts')
08

Server Actions

Server Actions let you define async functions that run on the server, callable directly from Client or Server Components. Mark them with "use server". They replace the need for separate API endpoints for mutations.

Inline Server Action

// app/posts/new/page.tsx (Server Component)
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';

export default function NewPost() {
  async function createPost(formData: FormData) {
    'use server';
    const title = formData.get('title') as string;
    await db.post.create({ data: { title } });
    revalidatePath('/posts');
    redirect('/posts');
  }

  return (
    <form action={createPost}>
      <input name="title" required />
      <button type="submit">Create</button>
    </form>
  );
}

Separate Actions File

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

import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';

export async function deletePost(id: string) {
  await db.post.delete({ where: { id } });
  revalidateTag('posts');
}

export async function toggleLike(postId: string) {
  await db.like.toggle({ postId });
  revalidateTag('posts');
}
  • Can be imported by Client Components
  • Type-safe with TypeScript
  • Progressive enhancement (works without JS)
  • Automatic request deduplication

Key Benefits

No manual API layer for mutations • Built-in CSRF protection • Works with forms natively (progressive enhancement) • Compose with revalidatePath and revalidateTag for cache invalidation

09

Rendering Strategies

Next.js supports multiple rendering strategies. The framework automatically chooses between static and dynamic based on your data fetching, but you can control it explicitly.

SSG — Static Site Generation

  • Pages rendered at build time
  • HTML cached on CDN
  • Fastest TTFB
  • Use for marketing pages, docs, blogs
// Static by default
export default async function Page() {
  const data = await fetch('...');
  return <div>{data}</div>;
}

SSR — Server-Side Rendering

  • Rendered on every request
  • Always fresh data
  • Higher TTFB than SSG
  • Use for personalised content
// Force dynamic rendering
export const dynamic = 'force-dynamic';

export default async function Page() {
  const data = await fetch('...', {
    cache: 'no-store'
  });
  return <div>{data}</div>;
}

ISR — Incremental Static Regen

  • Static + time-based revalidation
  • Stale-while-revalidate pattern
  • Best of SSG and SSR
  • Use for content that changes periodically
export const revalidate = 60;

export default async function Page() {
  const data = await fetch('...');
  return <div>{data}</div>;
}

Streaming with Suspense

import { Suspense } from 'react';

export default function Page() {
  return (
    <main>
      <h1>Dashboard</h1>
      <Suspense fallback={<Skeleton />}>
        <SlowDataComponent />
      </Suspense>
    </main>
  );
}

Partial Prerendering (PPR)

  • Combines static shell + dynamic holes
  • Static HTML served instantly from CDN
  • Dynamic parts stream in via Suspense
  • Experimental in Next.js 14/15
  • Best of all strategies in one route
10

Layouts & Templates

Next.js uses a nested layout system. Layouts wrap child segments and persist across navigations — they don't re-render or lose state when the user moves between sibling routes.

Root Layout (required)

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <nav>Site Navigation</nav>
        {children}
      </body>
    </html>
  );
}

Nested Layout

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex">
      <Sidebar />
      <main>{children}</main>
    </div>
  );
}

loading.tsx

// app/dashboard/loading.tsx
export default function Loading() {
  return <Skeleton />;
}
// Auto-wrapped in Suspense

error.tsx

'use client';
// app/dashboard/error.tsx
export default function Error({
  error, reset,
}: {
  error: Error; reset: () => void;
}) {
  return <button onClick={reset}>
    Retry</button>;
}

not-found.tsx

// app/dashboard/not-found.tsx
export default function NotFound() {
  return (
    <div>
      <h2>Not Found</h2>
      <p>Could not find resource</p>
    </div>
  );
}

Layout vs Template

Layouts persist state across navigations. Templates (template.tsx) create a new instance on every navigation — useful for animations, per-page analytics, or resetting form state.

11

Middleware

Next.js Middleware runs before a request is completed. It executes at the edge (close to the user) and can rewrite, redirect, modify headers, or block requests. Define it in middleware.ts at the project root.

Middleware Example

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

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session');

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

  // Add custom headers
  const response = NextResponse.next();
  response.headers.set('x-pathname',
    request.nextUrl.pathname);
  return response;
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
};

Common Use Cases

  • Authentication — redirect if no session
  • A/B testing — rewrite to variant pages
  • Geolocation — route by country/region
  • Rate limiting — block abusive requests
  • Bot detection — challenge suspicious traffic
  • i18n — redirect to locale-specific routes

Matcher Config

export const config = {
  matcher: [
    // Match all except static files
    '/((?!_next/static|_next/image|favicon.ico).*)',
    // Or specific paths
    '/dashboard/:path*',
    '/api/:path*',
  ],
};
12

API Routes / Route Handlers

Route Handlers let you create custom API endpoints using the Web Request and Response APIs. Define them in route.ts files inside the app/ directory.

Basic CRUD

// app/api/posts/route.ts
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';

export async function GET() {
  const posts = await db.post.findMany();
  return NextResponse.json(posts);
}

export async function POST(request: Request) {
  const body = await request.json();
  const post = await db.post.create({
    data: body
  });
  return NextResponse.json(post, { status: 201 });
}

Dynamic Route Handler

// app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server';

export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const post = await db.post.findUnique({
    where: { id: params.id }
  });
  if (!post) {
    return NextResponse.json(
      { error: 'Not found' }, { status: 404 }
    );
  }
  return NextResponse.json(post);
}

Streaming Response

// app/api/stream/route.ts
export async function GET() {
  const stream = new ReadableStream({
    async start(controller) {
      for (const chunk of data) {
        controller.enqueue(
          new TextEncoder().encode(chunk)
        );
        await wait(100);
      }
      controller.close();
    },
  });
  return new Response(stream);
}

Runtime Options

RuntimeUse Case
nodejs (default)Full Node.js APIs, DB drivers
edgeLow latency, limited APIs
// Force edge runtime
export const runtime = 'edge';

Supported HTTP methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS

13

Styling

Next.js supports multiple styling approaches out of the box. The choice depends on your team's preferences and whether components are Server or Client components.

CSS Modules (recommended)

/* app/components/Button.module.css */
.primary {
  background: #0070f3;
  color: white;
  border-radius: 6px;
  padding: 0.5rem 1rem;
}
import styles from './Button.module.css';

export function Button({ children }) {
  return (
    <button className={styles.primary}>
      {children}
    </button>
  );
}

Tailwind CSS

// Works in both Server & Client Components
export function Card({ title, description }) {
  return (
    <div className="rounded-lg border
      bg-white p-6 shadow-sm">
      <h3 className="text-lg font-semibold
        text-gray-900">{title}</h3>
      <p className="mt-2 text-gray-600">
        {description}
      </p>
    </div>
  );
}

Tailwind is the most popular choice in the Next.js ecosystem and is included by default in create-next-app.

Global Styles

// app/layout.tsx
import './globals.css';

export default function RootLayout({ children }) {
  return <html><body>{children}</body></html>;
}

CSS-in-JS Limitations

  • Libraries like styled-components, Emotion require "use client"
  • Cannot be used in Server Components
  • Runtime CSS-in-JS adds JS bundle weight
  • Consider CSS Modules or Tailwind for RSC-first apps
14

Image & Font Optimisation

Next.js provides built-in components for automatic image optimisation and zero-layout-shift font loading that significantly improve Core Web Vitals.

next/image

import Image from 'next/image';

export function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={1200}
      height={600}
      priority          // LCP image
      placeholder="blur" // blur-up effect
    />
  );
}

// Remote images
const nextConfig = {
  images: {
    remotePatterns: [{
      protocol: 'https',
      hostname: 'cdn.example.com',
    }],
  },
};

next/font

// app/layout.tsx
import { Inter, Playfair_Display }
  from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
});

const playfair = Playfair_Display({
  subsets: ['latin'],
  variable: '--font-playfair',
});

export default function RootLayout({ children }) {
  return (
    <html className={`${inter.variable}
      ${playfair.variable}`}>
      <body>{children}</body>
    </html>
  );
}

Image Features

  • Automatic WebP/AVIF conversion
  • Responsive srcSet generation
  • Lazy loading by default
  • Prevents Cumulative Layout Shift
  • On-demand resizing at the edge

Font Features

  • Self-hosted (no external requests)
  • Zero layout shift via size-adjust
  • Automatic subsetting
  • Works with Google Fonts & local files
  • CSS variable support for Tailwind
15

Metadata & SEO

Next.js provides a powerful Metadata API for defining SEO tags, Open Graph images, and structured data. Metadata can be static or dynamically generated per page.

Static Metadata

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

export const metadata: Metadata = {
  title: {
    default: 'My App',
    template: '%s | My App',
  },
  description: 'Built with Next.js',
  openGraph: {
    type: 'website',
    locale: 'en_US',
    url: 'https://myapp.com',
    siteName: 'My App',
  },
};

Dynamic Metadata

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';

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

JSON-LD

export default function Page() {
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: 'My Post',
    author: { '@type': 'Person',
      name: 'Author' },
  };
  return (
    <script type="application/ld+json"
      dangerouslySetInnerHTML={{
        __html: JSON.stringify(jsonLd),
      }}
    />
  );
}

Sitemap

// app/sitemap.ts
export default async function sitemap() {
  const posts = await getPosts();
  return [
    { url: 'https://myapp.com',
      lastModified: new Date() },
    ...posts.map(post => ({
      url: `https://myapp.com/blog/
        ${post.slug}`,
      lastModified: post.updatedAt,
    })),
  ];
}

robots.ts

// app/robots.ts
export default function robots() {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: '/admin/',
    },
    sitemap:
      'https://myapp.com/sitemap.xml',
  };
}
16

Authentication Patterns

Authentication in Next.js typically combines Middleware for route protection, Server Components for session checks, and a library like Auth.js (NextAuth.js v5) for the auth logic.

Auth.js Setup

// auth.ts
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import { PrismaAdapter } from
  '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';

export const { handlers, signIn, signOut, auth }
  = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [GitHub],
});

// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth';
export const { GET, POST } = handlers;

Middleware Protection

// middleware.ts
import { auth } from './auth';

export default auth((req) => {
  if (!req.auth &&
    req.nextUrl.pathname !== '/login') {
    const loginUrl = new URL('/login',
      req.nextUrl.origin);
    return Response.redirect(loginUrl);
  }
});

export const config = {
  matcher: ['/dashboard/:path*'],
};

Server Component Session

// app/dashboard/page.tsx
import { auth } from '@/auth';
import { redirect } from 'next/navigation';

export default async function Dashboard() {
  const session = await auth();
  if (!session) redirect('/login');

  return <h1>Welcome, {session.user.name}</h1>;
}

Auth Strategies

StrategyBest For
JWT sessionsStateless, edge-compatible
Database sessionsRevocable, secure
OAuth providersSocial login (GitHub, Google)
CredentialsEmail/password (use with care)
17

Database & ORM Integration

Next.js Server Components and Server Actions provide direct database access without an intermediate API layer. The two most popular ORMs in the ecosystem are Prisma and Drizzle.

Prisma

// prisma/schema.prisma
model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId],
                      references: [id])
  authorId  String
}

// lib/db.ts
import { PrismaClient } from '@prisma/client';

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

export const prisma = globalForPrisma.prisma
  || new PrismaClient();

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

Drizzle

// db/schema.ts
import { pgTable, text, boolean, timestamp }
  from 'drizzle-orm/pg-core';

export const posts = pgTable('posts', {
  id: text('id').primaryKey(),
  title: text('title').notNull(),
  content: text('content'),
  published: boolean('published').default(false),
  createdAt: timestamp('created_at')
    .defaultNow(),
});

// db/index.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});
export const db = drizzle(pool);

server-only Pattern

// lib/db.ts — prevent client import
import 'server-only';
import { prisma } from './prisma';

export async function getPosts() {
  return prisma.post.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
  });
}

Connection Pooling

  • Serverless functions create many connections
  • Use PgBouncer or Prisma Accelerate
  • Neon, Supabase, PlanetScale offer built-in pooling
  • Cache the client on globalThis in dev
  • Set connection_limit=1 per serverless instance
18

Testing

A solid Next.js testing strategy covers unit tests (components/utilities), integration tests (API routes, server actions), and end-to-end tests (full user flows).

Vitest + React Testing Library

// __tests__/components/button.test.tsx
import { render, screen } from
  '@testing-library/react';
import userEvent from
  '@testing-library/user-event';
import { Button } from '@/components/button';

describe('Button', () => {
  it('handles click events', async () => {
    const onClick = vi.fn();
    render(<Button onClick={onClick}>
      Click me</Button>);

    await userEvent.click(
      screen.getByRole('button'));
    expect(onClick).toHaveBeenCalledOnce();
  });
});

Playwright (E2E)

// e2e/blog.spec.ts
import { test, expect } from '@playwright/test';

test('can create a blog post', async ({ page }) => {
  await page.goto('/posts/new');
  await page.fill('[name="title"]',
    'Test Post');
  await page.fill('[name="content"]',
    'Hello world');
  await page.click('button[type="submit"]');

  await expect(page).toHaveURL(/\/posts\//);
  await expect(
    page.getByText('Test Post')
  ).toBeVisible();
});

MSW (Mock Service Worker)

// mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/posts', () => {
    return HttpResponse.json([
      { id: '1', title: 'Mock Post' },
    ]);
  }),
];

Testing Strategy

LayerToolWhat to Test
UnitVitestUtils, hooks, components
IntegrationVitest + RTLPages, API routes
E2EPlaywrightFull user flows
API mockingMSWExternal services
19

Deployment

Next.js can be deployed to Vercel (zero-config), self-hosted with Node.js, or containerised with Docker. Each option has different trade-offs for features like ISR, Middleware, and Edge Runtime.

Vercel (recommended)

  • Zero configuration deployment
  • Automatic edge network
  • Built-in ISR & image optimisation
  • Preview deployments per PR
  • Analytics & speed insights
  • Serverless & edge functions
# Deploy with Vercel CLI
npx vercel

Self-Hosted (Node.js)

  • Full control over infrastructure
  • All features supported
  • Requires own CDN for caching
  • Manual scaling & monitoring
# Build and start
npm run build
npm start
# Runs on port 3000 by default

Docker

FROM node:20-alpine AS base

FROM base AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static \
  ./.next/static

EXPOSE 3000
CMD ["node", "server.js"]

Environment Variables

PrefixAvailable
NEXT_PUBLIC_*Client + Server (inlined at build)
No prefixServer only (secrets safe)

Edge vs Node Runtime

AspectNode.jsEdge
Cold start~250ms~0ms
APIsFull Node.jsWeb APIs only
Max durationMinutesSeconds
Best forHeavy compute, DBAuth, redirects
20

Summary & Next Steps

Key Takeaways

  • App Router is the modern default — use it for new projects
  • Server Components reduce bundle size and enable direct data access
  • Server Actions eliminate boilerplate API routes for mutations
  • Multiple rendering strategies (SSG, SSR, ISR, streaming) per route
  • Built-in image, font, and metadata optimisation
  • Middleware runs at the edge for auth, redirects, and A/B tests
  • Deploy anywhere — Vercel, Docker, or bare Node.js

Essential Resources

Recommended Next Steps

  • Run npx create-next-app@latest and explore
  • Build a blog with App Router & Server Actions
  • Add Auth.js authentication
  • Deploy to Vercel with a database (Neon/Supabase)
  • Write E2E tests with Playwright
  • Experiment with Partial Prerendering

Thank you! — Built with Reveal.js · Single self-contained HTML file