Next.js 13+ Essentials: Key Concepts in a Nutshell
1. App Router: File-Based Routing
Starting with Next.js 13, the App Router was introduced as a more powerful and flexible way to handle routing, replacing the older Pages Router for new projects.
Key Concepts:
- File-based routing using the
/appdirectory. - Built on top of React Server Components.
- Support for nested layouts, loading states, error boundaries, and parallel routes.
Each file or folder inside the app/ directory automatically maps to a route in the app. Each route must contain a page.tsx (or page.jsx/tsx) file, which is the component rendered for that path.
You can define layout.tsx files at any level. Layouts wrap page components and persist across navigation.
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<body>
<header>My Site Header</header>
{children}
<footer>My Site Footer</footer>
</body>
</html>
);
}// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
return (
<div style={{ display: 'flex' }}>
<aside>Sidebar</aside>
<main>{children}</main>
</div>
);
}// app/dashboard/settings/page.tsx
export default function SettingsPage() {
return <div>Settings</div>;
}You can use square brackets to create dynamic segments: [id] folder in above structure. Route groups (folder names in parentheses) help organize code without affecting the URL path: (auth) in above structure.
Additionally you can add loading.tsx to show a loading spinner while the route or data is being fetched. Furthermore, error.tsx can be used to gracefully handle errors specific to a route.
2. Server Components: React Server Components (RSC)
React Server Components (RSC) are components that run only on the server that never get sent to the client. They allow you to fetch data and render HTML on the server without increasing the size of the JavaScript bundle sent to the browser.
In the App Router, all components are Server Components by default. If a component uses browser-only features like useState, useEffect, or event listeners, you must mark it explicitly with, 'use client' at the top.
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}In the App Router, you can use native fetch() in Server Components (or async functions). It automatically works with caching and streaming.
// server side component, not 'use client' needed
async function getProducts() {
const res = await fetch('https://api.example.com/products');
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<ul>
{products.map((p) => <li key={p.id}>{p.name}</li>)}
</ul>
);
}You can use cache: 'no-store' to ensure fresh data on every request. (Server Side Rendering)
export default async function UsersPage() {
const res = await fetch('https://api.example.com/users', {
cache: 'no-store', // disables caching → SSR behavior
});
const users = await res.json();
return (
<ul>
{users.map((u: any) => <li key={u.id}>{u.name}</li>)}
</ul>
);
}Client Side Fetching
SWR — A lightweight React hook library for client-side fetching with caching.
'use client';
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then((res) => res.json());
export default function Profile() {
const { data, error } = useSWR('/api/profile', fetcher);
if (!data) return <div>Loading...</div>;
return <div>Hello {data.name}</div>;
}Tanstack (React-query)— A powerful client-side library for caching, background updating, and syncing server state.
'use client';
import { useQuery } from '@tanstack/react-query';
function Users() {
const { data, isLoading } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json())
});
if (isLoading) return <p>Loading...</p>;
return <ul>{data.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}3. Incremental Static Regeneration (ISR): Update static pages post-build
ISR allows you to update static pages after the site has been built, without needing a full rebuild or redeploy.
- Pages are pre-rendered at build time.
- When a user visits a page, Next.js checks if it’s time to revalidate (based on a timer).
- If so, it re-generates the page in the background and serves the updated content next time.
In the App Router, you can use fetch() with a next config:
// app/blog/page.tsx
export default async function BlogPage() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }, // Regenerate this page every 60 seconds
});
const posts = await res.json();
return (
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}3. Full-Stack Features: API routes
In Next.js 13+, using the App Router, you define API routes inside the app/api/ directory using server-only route handlers. Instead of writing traditional Express-style handlers like in pages/api/, you export HTTP method functions (GET, POST, PUT, DELETE) from a special file: route.ts or route.js.
// app/api/hello/route.ts
export async function GET() {
return Response.json({ message: "Hello from App Router API!" });
}Then, /api/hello will return : {"message": "Hello from App Router API!"}
Get Imesh Udantha Thanapathi’s stories in your inbox
Join Medium for free to get updates from this writer.
You can use Request.json() to parse incoming JSON, and Response.json() to send responses.
// app/api/user/route.ts
export async function POST(req: Request) {
const body = await req.json();
const { name } = body;
return Response.json({
message: `Hello, ${name}!`,
});
}
// frontend call
await fetch('/api/user', {
method: 'POST',
body: JSON.stringify({ name: 'Alex' }),
headers: {
'Content-Type': 'application/json',
},
});You can use route parameters with [param] like in the page routing system. Ex: app/api/posts/[id]/route.ts Route:/api/posts/123
4. NextAuth.js — Authentication for Next.js
NextAuth.js is the most popular authentication library for Next.js. It provides built-in support for:
- OAuth (Google, GitHub, etc.)
- Credentials (email/password)
- JWT-based auth
- Session management
Example Config: Google Provider
Installing
npm install next-auth// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
const handler = NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
callbacks: {
async session({ session, token }) {
session.user.id = token.sub;
return session;
},
},
});
export { handler as GET, handler as POST };Using Auth in Components,
// app/login/page.tsx
import { useSession, signIn, signOut } from "next-auth/react";
const { data: session } = useSession();
signIn('google'); // Trigger sign-in
signOut(); // Trigger sign-outMiddleware for Route Protection
Middleware runs before a request reaches a route. It can be used to,
- Block unauthorized users
- Redirect users
- Add custom headers
// middleware.ts in project root
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from "next-auth/jwt";
export async function middleware(req: NextRequest) {
// Checks for a valid session token
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
const isAuthPage = req.nextUrl.pathname.startsWith('/login');
// if there is no token or user not logged in, redirected to /login
if (!token && !isAuthPage) {
return NextResponse.redirect(new URL('/login', req.url));
}
return NextResponse.next();
}
// Apply to all routes or specify
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*'], // protected routes
};5. Server Actions & Mutations: Next.js 14 feature
A new way to handle mutations (like form submissions or DB updates) directly in Server Components. This eliminates the need of API routes. This feature only works in Next.js App Router.
Server Actions let you:
- Define async server functions (like form handlers)
- Call them directly from client/server components
- Avoid creating API routes manually
- Mutate data securely, server-side
// app/actions.ts
'use server'; // Actions must include 'use server' directive
import { prisma } from './lib/prisma';
export async function createUser(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
// Prisma is a ORM for SQL databases.
// Prisma talks to → PlanetScale which is a serverless MySql for Next.js
await prisma.user.create({ data: { name, email } });
}import { createUser} from '../actions';
...
<form action={createUser}>
<input name="name" />
<input name="email" />
<button type="submit">Submit</button>
</form>
...6. Image Optimization
Next.js provides the <Image /> component from next/image for Automatic image resizing, Lazy loading (by default), Responsive images, Built-in CDN support, WebP/ AVIF conversion, Blur-up placeholder.
Lazy loading means the image load only when visible in the viewport. It helps to improve the performance on initial page load.
Basic Usage
import Image from 'next/image';
export default function Profile() {
return (
<Image
src="/images/me.jpg" // Static or remote image
alt="My profile picture"
width={400}
height={400}
/>
);
}Responsive Images
<div className="relative w-full h-64">
<Image
src="/image.jpg"
alt="Responsive"
fill
sizes="(max-width: 768px) 100vw, 33vw"
style={{ objectFit: 'cover' }}
/>
</div>Blur Placeholder
<Image
src="/image.jpg"
alt="..."
width={400}
height={300}
placeholder="blur"
blurDataURL="/blur-image.jpg"
/>Next.js has truly transformed web development by combining powerful full-stack features with top-tier performance optimizations. With its modern routing, server components, built-in image optimization, and seamless authentication, it makes building scalable, production-ready applications easier than ever. So, get started with Next.js today and build your dreamy websites with ease. Happy Coding!







