Unlocking the Full Power of Next.js 15's File-Based Routing System
Next.js has always been celebrated for its intuitive file-based routing, but the App Router in Next.js 15 takes this concept to unprecedented levels of sophistication. While the basics of creating a page by adding a file seem simple, developers often struggle with more advanced routing patterns: organizing complex applications with route groups, handling dynamic segments efficiently, implementing parallel routes for sophisticated layouts, and intercepting routes for modal experiences.
This comprehensive guide takes you from fundamental routing concepts to advanced patterns that power production applications. You'll learn how to structure your file system for optimal organization, create dynamic product pages that scale, build dashboard layouts with parallel data streams, implement modal interactions using intercepting routes, and master route handlers for API endpoints. Whether you're building a simple blog or a complex e-commerce platform, you'll gain the routing expertise needed to architect maintainable Next.js applications.
Prerequisites
Before exploring advanced routing patterns, ensure you have:
-
Next.js 15 installed -
npx create-next-app@latest my-app -
Basic Next.js App Router knowledge - Understanding of
page.tsxandlayout.tsx - TypeScript fundamentals - Interfaces, types, and basic syntax
- React knowledge - Components, props, and basic hooks
- File system concepts - Understanding folders, paths, and directory structures
- HTTP methods - GET, POST, PUT, DELETE basics
- Code editor - VS Code with Next.js snippets extension recommended
The Foundation: File-Based Routing Basics
Next.js uses your file system structure to define routes automatically. Each folder in the app directory represents a URL segment, and special files define the UI for that route.
Core Routing Files
app/
├── page.tsx // Route: /
├── layout.tsx // Applies to / and all children
├── loading.tsx // Loading UI for /
├── error.tsx // Error UI for /
├── not-found.tsx // 404 UI for /
└── about/
└── page.tsx // Route: /about
Let's build a basic routing structure:
// app/page.tsx
export default function HomePage() {
return (
<div>
<h1>Welcome to Our Store</h1>
<p>Discover amazing products</p>
</div>
);
}
// app/layout.tsx
import { ReactNode } from 'react';
import './globals.css';
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<nav>
<a href="/">Home</a>
<a href="/products">Products</a>
<a href="/about">About</a>
</nav>
<main>{children}</main>
</body>
</html>
);
}
Understanding Route Segments
Each folder creates a URL segment:
app/
├── blog/
│ ├── page.tsx // /blog
│ └── authors/
│ └── page.tsx // /blog/authors
└── products/
├── page.tsx // /products
└── categories/
└── page.tsx // /products/categories
Dynamic Routes: Building Scalable URL Patterns
Dynamic routes allow you to create pages for dynamic data without creating individual files for each item.
Single Dynamic Segment
Create a dynamic segment using square brackets:
app/
└── products/
└── [id]/
└── page.tsx // Matches /products/1, /products/abc, etc.
// app/products/[id]/page.tsx
interface ProductPageProps {
params: {
id: string;
};
}
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 3600 }
});
if (!res.ok) throw new Error('Failed to fetch product');
return res.json();
}
export default async function ProductPage({ params }: ProductPageProps) {
const product = await getProduct(params.id);
return (
<div className="product-container">
<h1>{product.name}</h1>
<img src={product.image} alt={product.name} />
<p className="price">${product.price}</p>
<p className="description">{product.description}</p>
<button>Add to Cart</button>
</div>
);
}
Generating Static Params
For static generation with dynamic routes, use generateStaticParams:
// app/products/[id]/page.tsx
export async function generateStaticParams() {
const products = await fetch('https://api.example.com/products').then(
res => res.json()
);
return products.map((product: any) => ({
id: product.id.toString(),
}));
}
// This generates static pages at build time for all products
export default async function ProductPage({ params }: ProductPageProps) {
const product = await getProduct(params.id);
return <div>{product.name}</div>;
}
Multiple Dynamic Segments
Create complex URL patterns with multiple dynamic segments:
app/
└── blog/
└── [category]/
└── [slug]/
└── page.tsx // Matches /blog/tech/nextjs-guide
// app/blog/[category]/[slug]/page.tsx
interface BlogPostProps {
params: {
category: string;
slug: string;
};
}
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(
res => res.json()
);
return posts.map((post: any) => ({
category: post.category,
slug: post.slug,
}));
}
export default async function BlogPost({ params }: BlogPostProps) {
const post = await fetch(
`https://api.example.com/posts/${params.category}/${params.slug}`
).then(res => res.json());
return (
<article>
<div className="breadcrumb">
<a href="/blog">Blog</a> /
<a href={`/blog/${params.category}`}>{params.category}</a> /
<span>{post.title}</span>
</div>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Catch-All Segments
Match multiple path segments with catch-all routes:
app/
└── shop/
└── [...slug]/
└── page.tsx // Matches /shop/a, /shop/a/b, /shop/a/b/c
// app/shop/[...slug]/page.tsx
interface ShopPageProps {
params: {
slug: string[]; // Array of path segments
};
}
export default async function ShopPage({ params }: ShopPageProps) {
// /shop/electronics/phones/iphone -> ['electronics', 'phones', 'iphone']
const { slug } = params;
// Determine what to show based on depth
if (slug.length === 1) {
// Category page: /shop/electronics
return <CategoryPage category={slug[0]} />;
} else if (slug.length === 2) {
// Subcategory page: /shop/electronics/phones
return <SubcategoryPage category={slug[0]} subcategory={slug[1]} />;
} else if (slug.length === 3) {
// Product page: /shop/electronics/phones/iphone
return <ProductDetailPage path={slug} />;
}
return <div>Shop</div>;
}
Optional Catch-All Segments
Match zero or more segments:
app/
└── docs/
└── [[...slug]]/
└── page.tsx // Matches /docs, /docs/a, /docs/a/b
// app/docs/[[...slug]]/page.tsx
interface DocsPageProps {
params: {
slug?: string[]; // Optional array
};
}
export default async function DocsPage({ params }: DocsPageProps) {
const slug = params.slug || [];
// /docs -> Show documentation home
if (slug.length === 0) {
return <DocsHome />;
}
// /docs/api/authentication -> Show specific doc
const docPath = slug.join('/');
const doc = await fetchDoc(docPath);
return (
<div className="docs-container">
<aside>
<DocsSidebar currentPath={docPath} />
</aside>
<article>
<h1>{doc.title}</h1>
<div dangerouslySetInnerHTML={{ __html: doc.content }} />
</article>
</div>
);
}
Route Groups: Organizing Your Application
Route groups let you organize routes without affecting the URL structure. Create a route group by wrapping a folder name in parentheses.
Basic Route Groups
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx // URL: /about (not /marketing/about)
│ ├── contact/
│ │ └── page.tsx // URL: /contact
│ └── layout.tsx // Applies to marketing pages only
└── (shop)/
├── products/
│ └── page.tsx // URL: /products (not /shop/products)
└── layout.tsx // Applies to shop pages only
// app/(marketing)/layout.tsx
export default function MarketingLayout({
children
}: {
children: React.ReactNode
}) {
return (
<div>
<header className="marketing-header">
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
</header>
{children}
<footer className="marketing-footer">
<p>© 2026 Our Company</p>
</footer>
</div>
);
}
// app/(shop)/layout.tsx
export default function ShopLayout({
children
}: {
children: React.ReactNode
}) {
return (
<div>
<header className="shop-header">
<nav>
<a href="/products">Products</a>
<a href="/cart">Cart</a>
<a href="/account">Account</a>
</nav>
</header>
<div className="shop-content">
<aside className="shop-sidebar">
<FilterPanel />
</aside>
<main>{children}</main>
</div>
</div>
);
}
Multiple Root Layouts
Route groups enable multiple root layouts in your application:
app/
├── (auth)/
│ ├── layout.tsx // Auth-specific root layout
│ ├── login/
│ │ └── page.tsx
│ └── register/
│ └── page.tsx
└── (dashboard)/
├── layout.tsx // Dashboard-specific root layout
├── page.tsx
└── settings/
└── page.tsx
// app/(auth)/layout.tsx
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className="auth-body">
<div className="auth-container">
<div className="auth-card">
{children}
</div>
</div>
</body>
</html>
);
}
// app/(dashboard)/layout.tsx
export default function DashboardLayout({
children
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className="dashboard-body">
<nav className="dashboard-nav">
<a href="/dashboard">Dashboard</a>
<a href="/dashboard/analytics">Analytics</a>
<a href="/dashboard/settings">Settings</a>
</nav>
<main className="dashboard-main">
{children}
</main>
</body>
</html>
);
}
Organizing by Feature
Use route groups to organize by feature while maintaining flat URLs:
app/
├── (products)/
│ ├── products/
│ │ └── page.tsx // /products
│ ├── products/[id]/
│ │ └── page.tsx // /products/[id]
│ └── categories/
│ └── page.tsx // /categories
└── (users)/
├── profile/
│ └── page.tsx // /profile
└── settings/
└── page.tsx // /settings
Parallel Routes: Loading Multiple Pages Simultaneously
Parallel routes allow you to render multiple pages in the same layout simultaneously, each with independent loading and error states.
Creating Parallel Routes
Define parallel routes using the @folder convention:
app/
└── dashboard/
├── @analytics/
│ ├── page.tsx
│ └── loading.tsx
├── @notifications/
│ ├── page.tsx
│ └── loading.tsx
├── layout.tsx
└── page.tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
notifications,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
notifications: React.ReactNode;
}) {
return (
<div className="dashboard-grid">
<div className="dashboard-main">
{children}
</div>
<div className="dashboard-analytics">
{analytics}
</div>
<div className="dashboard-notifications">
{notifications}
</div>
</div>
);
}
// app/dashboard/@analytics/page.tsx
async function getAnalytics() {
const res = await fetch('https://api.example.com/analytics', {
next: { revalidate: 300 } // Revalidate every 5 minutes
});
return res.json();
}
export default async function AnalyticsSlot() {
const data = await getAnalytics();
return (
<div className="analytics-panel">
<h2>Analytics</h2>
<div className="metrics">
<div className="metric">
<span className="metric-label">Page Views</span>
<span className="metric-value">{data.pageViews}</span>
</div>
<div className="metric">
<span className="metric-label">Users</span>
<span className="metric-value">{data.users}</span>
</div>
</div>
</div>
);
}
// app/dashboard/@analytics/loading.tsx
export default function AnalyticsLoading() {
return (
<div className="analytics-panel">
<h2>Analytics</h2>
<div className="loading-skeleton">
<div className="skeleton-line"></div>
<div className="skeleton-line"></div>
</div>
</div>
);
}
Conditional Rendering with Parallel Routes
Show different content based on user state:
// app/dashboard/layout.tsx
import { getUser } from '@/lib/auth';
export default async function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
const user = await getUser();
return (
<div className="dashboard">
{children}
{analytics}
{user.role === 'admin' && team}
</div>
);
}
Default Parallel Routes
Provide a default when no matching page exists:
app/
└── dashboard/
├── @analytics/
│ ├── default.tsx // Fallback for @analytics
│ └── page.tsx
└── layout.tsx
// app/dashboard/@analytics/default.tsx
export default function AnalyticsDefault() {
return (
<div className="analytics-panel">
<p>Analytics not available for this view</p>
</div>
);
}
Intercepting Routes: Creating Modal Experiences
Intercepting routes allow you to load a route within the current layout while still preserving the URL for sharing and refreshing.
Intercepting Route Conventions
-
(.)- Match segments on the same level -
(..)- Match segments one level above -
(..)(..)- Match segments two levels above -
(...)- Match segments from the root
Building a Photo Gallery with Modals
app/
└── photos/
├── page.tsx // Gallery view
├── [id]/
│ └── page.tsx // Full photo page
└── (.)[id]/
└── page.tsx // Intercepted modal view
// app/photos/page.tsx
import Link from 'next/link';
async function getPhotos() {
const res = await fetch('https://api.example.com/photos');
return res.json();
}
export default async function PhotosPage() {
const photos = await getPhotos();
return (
<div className="photo-grid">
{photos.map((photo: any) => (
<Link key={photo.id} href={`/photos/${photo.id}`}>
<img src={photo.thumbnail} alt={photo.title} />
</Link>
))}
</div>
);
}
// app/photos/(.)[id]/page.tsx - Modal view
import { Modal } from '@/components/Modal';
async function getPhoto(id: string) {
const res = await fetch(`https://api.example.com/photos/${id}`);
return res.json();
}
export default async function PhotoModal({
params
}: {
params: { id: string }
}) {
const photo = await getPhoto(params.id);
return (
<Modal>
<img src={photo.url} alt={photo.title} className="modal-image" />
<h2>{photo.title}</h2>
<p>{photo.description}</p>
</Modal>
);
}
// app/photos/[id]/page.tsx - Full page view
async function getPhoto(id: string) {
const res = await fetch(`https://api.example.com/photos/${id}`);
return res.json();
}
export default async function PhotoPage({
params
}: {
params: { id: string }
}) {
const photo = await getPhoto(params.id);
return (
<div className="photo-page">
<img src={photo.url} alt={photo.title} />
<div className="photo-details">
<h1>{photo.title}</h1>
<p>{photo.description}</p>
<div className="photo-meta">
<span>Uploaded: {photo.uploadDate}</span>
<span>Views: {photo.views}</span>
</div>
</div>
</div>
);
}
// components/Modal.tsx
'use client';
import { useRouter } from 'next/navigation';
import { useEffect, useRef } from 'react';
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter();
const dialogRef = useRef<HTMLDialogElement>(null);
useEffect(() => {
dialogRef.current?.showModal();
}, []);
const handleClose = () => {
router.back();
};
return (
<dialog ref={dialogRef} onClose={handleClose} className="photo-modal">
<button onClick={handleClose} className="close-button">×</button>
{children}
</dialog>
);
}
E-commerce Product Quick View
app/
└── products/
├── page.tsx // Product listing
├── [id]/
│ └── page.tsx // Full product page
└── (.)[id]/
└── page.tsx // Quick view modal
// app/products/(.)[id]/page.tsx
import { Modal } from '@/components/Modal';
import { AddToCartButton } from '@/components/AddToCartButton';
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`);
return res.json();
}
export default async function ProductQuickView({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id);
return (
<Modal>
<div className="quick-view">
<div className="quick-view-image">
<img src={product.image} alt={product.name} />
</div>
<div className="quick-view-details">
<h2>{product.name}</h2>
<p className="price">${product.price}</p>
<p className="description">{product.shortDescription}</p>
<AddToCartButton productId={product.id} />
<a href={`/products/${product.id}`} className="view-full">
View Full Details →
</a>
</div>
</div>
</Modal>
);
}
Route Handlers: Building API Endpoints
Route handlers allow you to create custom API endpoints using Web Request and Response APIs.
Basic Route Handler
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const products = await fetchProducts();
return NextResponse.json({
success: true,
data: products,
});
}
export async function POST(request: NextRequest) {
const body = await request.json();
const newProduct = await createProduct(body);
return NextResponse.json({
success: true,
data: newProduct,
}, { status: 201 });
}
Dynamic Route Handlers
// app/api/products/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
interface RouteParams {
params: {
id: string;
};
}
export async function GET(
request: NextRequest,
{ params }: RouteParams
) {
const product = await fetchProduct(params.id);
if (!product) {
return NextResponse.json(
{ success: false, error: 'Product not found' },
{ status: 404 }
);
}
return NextResponse.json({
success: true,
data: product,
});
}
export async function PATCH(
request: NextRequest,
{ params }: RouteParams
) {
const body = await request.json();
const updatedProduct = await updateProduct(params.id, body);
return NextResponse.json({
success: true,
data: updatedProduct,
});
}
export async function DELETE(
request: NextRequest,
{ params }: RouteParams
) {
await deleteProduct(params.id);
return NextResponse.json({
success: true,
message: 'Product deleted',
});
}
Accessing Request Data
// app/api/search/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
// Get query parameters
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('q');
const page = searchParams.get('page') || '1';
const limit = searchParams.get('limit') || '10';
// Get headers
const authToken = request.headers.get('authorization');
// Get cookies
const cookies = request.cookies;
const sessionId = cookies.get('sessionId')?.value;
if (!authToken) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const results = await searchProducts({
query,
page: parseInt(page),
limit: parseInt(limit),
});
return NextResponse.json({
success: true,
data: results,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: results.total,
},
});
}
Setting Response Headers and Cookies
// app/api/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const { email, password } = await request.json();
const user = await authenticateUser(email, password);
if (!user) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
const token = generateToken(user);
const response = NextResponse.json({
success: true,
user: {
id: user.id,
email: user.email,
name: user.name,
},
});
// Set cookie
response.cookies.set('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
});
// Set custom headers
response.headers.set('X-User-Id', user.id);
return response;
}
Streaming Responses
// app/api/stream/route.ts
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
controller.enqueue(encoder.encode(`data: Message ${i}\n\n`));
}
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
Best Practices Section
✅ Dos
Use route groups for organization - Keep your file structure clean and maintainable without affecting URLs:
// ✅ Good - Organized by feature
app/
├── (auth)/
│ ├── login/
│ └── register/
└── (dashboard)/
├── analytics/
└── settings/
Leverage generateStaticParams for dynamic routes - Pre-render pages at build time for better performance:
// ✅ Good - Static generation
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map(post => ({ slug: post.slug }));
}
Implement proper loading and error states - Use loading.tsx and error.tsx files for better UX:
// app/products/loading.tsx
export default function Loading() {
return <ProductGridSkeleton />;
}
// app/products/error.tsx
'use client';
export default function Error({ error, reset }: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
Use intercepting routes for modal experiences - Preserve URLs while showing modals for better sharing and navigation:
// ✅ Good - Modal with shareable URL
app/
└── photos/
├── [id]/page.tsx // Full page
└── (.)[id]/page.tsx // Modal intercept
Validate and sanitize route handler inputs - Always validate incoming data:
// ✅ Good - Input validation
export async function POST(request: NextRequest) {
const body = await request.json();
if (!body.email || !isValidEmail(body.email)) {
return NextResponse.json(
{ error: 'Invalid email' },
{ status: 400 }
);
}
// Process valid data
}
❌ Don'ts
Don't create unnecessary nesting - Keep routes as flat as possible:
// ❌ Bad - Too nested
app/pages/products/categories/electronics/page.tsx
// ✅ Good - Flatter structure
app/categories/[slug]/page.tsx
Don't forget to handle errors in route handlers - Always include error handling:
// ❌ Bad - No error handling
export async function GET() {
const data = await fetchData(); // Can throw
return NextResponse.json(data);
}
// ✅ Good - Proper error handling
export async function GET() {
try {
const data = await fetchData();
return NextResponse.json({ success: true, data });
} catch (error) {
console.error('API Error:', error);
return NextResponse.json(
{ success: false, error: 'Internal server error' },
{ status: 500 }
);
}
}
Don't use route handlers for server-side rendering - Use Server Components instead:
// ❌ Bad - Using API route for data
'use client';
export default function Page() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData);
}, []);
return <div>{data?.title}</div>;
}
// ✅ Good - Server Component
export default async function Page() {
const data = await fetchData();
return <div>{data.title}</div>;
}
Don't expose sensitive data in parallel routes - Remember all slots are exposed to the client:
// ❌ Bad - Sensitive data in parallel route
app/dashboard/@admin/page.tsx // Admin data visible to all users
// ✅ Good - Check permissions in layout
export default async function Layout({ admin }) {
const user = await getUser();
return user.isAdmin ? admin : null;
}
Common Pitfalls
Pitfall 1: Forgetting default.tsx for parallel routes
When navigating to a route that doesn't have a matching parallel route page, Next.js needs a fallback:
// ❌ Bad - Missing default
app/dashboard/
├── @analytics/page.tsx
└── layout.tsx
// Navigate to /dashboard/settings -> Error!
// ✅ Good - Include default
app/dashboard/
├── @analytics/
│ ├── page.tsx
│ └── default.tsx // Fallback
└── layout.tsx
Pitfall 2: Incorrect intercepting route conventions
// ❌ Bad - Wrong intercept level
app/photos/
└── (..)[id]/page.tsx // Goes up one level, not same level
// ✅ Good - Same level intercept
app/photos/
└── (.)[id]/page.tsx // Correct for same level
Pitfall
3: Not handling the back button in modals
// ❌ Bad - No back button handling
export function Modal({ children }) {
return <div className="modal">{children}</div>;
}
// ✅ Good - Proper router integration
'use client';
import { useRouter } from 'next/navigation';
export function Modal({ children }) {
const router = useRouter();
return (
<div className="modal" onClick={() => router.back()}>
{children}
</div>
);
}
Conclusion
Mastering Next.js routing unlocks the full potential of the App Router architecture. From basic file-based routing to advanced patterns like parallel and intercepting routes, you now have the knowledge to build sophisticated, scalable applications with clean and maintainable code structures.
The key takeaways: use dynamic routes with generateStaticParams for optimal performance, organize complex applications with route groups without affecting URLs, leverage parallel routes for loading multiple data sources simultaneously, implement intercepting routes for seamless modal experiences, and create robust API endpoints with route handlers. Each pattern serves specific use cases, and choosing the right combination makes the difference between a good application and a great one.
Remember that routing is not just about URLs—it's about creating intuitive user experiences, organizing your codebase effectively, and optimizing performance. Start with simple patterns and gradually incorporate advanced techniques as your application grows. The file-based routing system may seem magical at first, but understanding these patterns gives you complete control over how your application behaves.
Resources
GitHub Repository
- Next.js Routing Examples - Official routing examples
- Complete E-commerce Routing Demo - All patterns from this guide
Official Documentation
- Next.js Routing Fundamentals - Comprehensive routing guide
- Route Handlers Documentation - API endpoints reference
- Parallel Routes Guide - Advanced parallel routing
Related Articles
- "Next.js 15 App Router: Complete Guide to Server and Client Components" - Understand component architecture
- "Building Authentication Systems in Next.js with Route Protection" - Secure your routes
- "Next.js Middleware: Advanced Request Handling and Route Protection" - Route-level logic
Meta Description: Master Next.js 15 routing with dynamic routes, route groups, parallel routes, and intercepting routes. Complete guide with e-commerce examples and best practices.
Tags: #nextjs #routing #webdev #react #typescript #apirouter #tutorial
Top comments (0)