Choosing the right frontend architecture determines how fast your e-commerce runs and how easily it scales. In the React ecosystem, Next.js sets the standard for building high-performance applications. A key part of that stack is the path management system, which makes URL structure intuitive to design. Understanding how routing works—especially with the modern App Router—lets you build stable, SEO-optimized services that load almost instantly.
What is file-system routing in Next.js?
Traditional React applications rely on external libraries such as React Router to define paths and maintain configuration by hand. Next.js removes that burden with file-system routing. In practice, your application's URL structure mirrors the folder structure on disk.
In this model, every folder in the root directory represents a path segment. For a URL to render in the browser, the folder must contain a page.js or page.tsx file. Developers can freely place other files inside folders—helper components, tests, or styles—without accidentally exposing them as separate routes.
Here is an example of a basic directory structure in a project using src/app, which is worth understanding when configuring a Next.js 13 project:
src/
└── app/
├── page.tsx # Home view (/)
├── o-nas/
│ └── page.tsx # Route (/o-nas)
└── kontakt/
├── page.tsx # Route (/kontakt)
└── kontakt.css # Private stylesheet
This layout makes information architecture management extremely transparent. New developers can understand the site structure immediately by browsing the directory tree.
App Router vs Pages Router — the evolution of path handling
For many years, the Pages Router was the Next.js standard: each route was defined by a file in the /pages directory. That approach had architectural limits. Nested layouts were hard to implement without re-rendering shared UI elements. The whole app also leaned on client-side rendering or classic SSR, which pushed large amounts of JavaScript to the browser.
The App Router, introduced with the changes in Next.js 13, addressed those issues. The new routing system in /app redefined how applications are built.
Its foundation is React Server Components in Next.js. Components render on the server by default, which means smaller bundles, better performance, and immediate content delivery.
Main differences between the two approaches:
- Defining routes: In the Pages Router, the route was a file (e.g.
o-nas.tsx); in the App Router, it is a folder with apage.tsxfile. - Component type: The older router defaults to Client Components; the new one defaults to Server Components.
- Layouts: The Pages Router relied on templates in
_app.tsx; the App Router offers nativelayout.tsxfiles. - Data fetching: Methods like
getServerSidePropswere replaced by standardfetchcalls in server components.
Special files in the App Router and their roles
In the App Router, Next.js uses a set of reserved filenames that let you declaratively define UI behavior for a given route segment. Each file plays a unique role in the rendering hierarchy and enables advanced UX patterns without complex state logic.
layout.js vs template.js — managing shared UI and state
The layout.tsx file defines a shared layout for multiple views in a segment (e.g. a fixed header, sidebar, or footer). A key trait of layouts is that they preserve state and do not re-render when users navigate between routes within the same layout. If the layout contains a search field with entered text, that text will not disappear when moving to another route in the same directory.
The template.tsx file works similarly but behaves differently during navigation. For each route, it creates a completely new component instance. On every URL change, the template state resets and side effects (such as useEffect) run again. Templates are used less often than layouts; they are mainly useful for entry animations or when page views must be recorded in external analytics tools on every transition.
Loading and error handling — loading.js, error.js, and not-found.js
Modern applications require attention to transitional states and resilience to network errors. Next.js provides dedicated special files that integrate automatically with React mechanisms:
loading.tsx: Automatically wraps a route segment in a React Suspense boundary. While data is loading, users see a loading state—for example, a skeleton screen. This works especially well with asynchronous data fetching in Next.js.error.tsx: Acts as an Error Boundary. If a runtime error occurs in a segment, the app does not crash entirely. Instead, the UI fromerror.tsxis rendered (marked with the'use client'directive), often with a retry button.not-found.tsx: Defines a dedicated 404 view for a specific segment or globally for the whole app. It is triggered when the requested resource does not exist or whennotFound()is called in code.
Dynamic routing in practice — handling variable URL parameters
In e-commerce projects, URLs are rarely static. Product pages, blog posts, and categories require variable parameters. Next.js handles this with dynamic segments, defined by placing a folder name in square brackets.
There are three main types of dynamic routes:
- Basic dynamic segment
[slug]: Matches a single variable path element. For example, an[id]folder matches/uzytkownik/123. The parameter is passed to the component as aparamsobject.
- Catch-all segment
[...slug]: Matches all subsequent nested path segments. A[...kategoria]folder matches both/sklep/butyand/sklep/buty/sportowe/meskie. In the component, parameters are available as an array of strings:['buty', 'sportowe', 'meskie'].
- Optional catch-all segment
[[...slug]]: Works like catch-all but also matches the base path without extra parameters. A[[...filtry]]folder matches both/katalogand/katalog/rozmiar-m/kolor-czarny. This is useful when building product filtering systems.
Here is an example of a dynamic product page in TypeScript showing how to read parameters from the URL:
interface ProductPageProps {
params: { slug: string };
}
export default function ProductPage({ params }: ProductPageProps) {
return (
<div className="product-container">
<h1>Product: {params.slug}</h1>
</div>
);
}
Navigation and performance — the Link component and useRouter hook
Moving between routes in Next.js is optimized for maximum performance and smooth interaction. The primary tool for declarative navigation is the <Link> component, which extends the standard HTML <a> tag. Instead of triggering a full browser reload, it intercepts the click and loads only the data needed for the new route, keeping the app in Single Page Application (SPA) mode.
One of the most important features of <Link> is automatic prefetching. As soon as a link enters the viewport, Next.js fetches the related route's code and data in the background. When the user actually clicks, navigation feels instant, with no noticeable delay. This mechanism plays a key role in overall page performance optimization, significantly improving Core Web Vitals.
When navigation must happen programmatically—for example, after a successful order—you use the useRouter hook from next/navigation. Because this hook runs on the client, the component that uses it must include 'use client' at the top of the file.
"use client";
import { useRouter } from "next/navigation";
export default function AddToCartButton() {
const router = useRouter();
return (
<button onClick={() => router.push("/koszyk")} className="btn-primary">
Go to summary
</button>
);
}
Next.js routing in headless Shopify — mapping products and collections
In headless e-commerce, where Next.js handles presentation and Shopify manages data and sales processes, correct path mapping is critical. When you choose the benefits of Shopify Headless, you need to mirror URL structure so it works for users and search engine crawlers.
Shopify's standard URL scheme uses /products/[handle] for product pages and /collections/[handle] for categories. In a Next.js project, you create a matching folder structure inside src/app. With server components, you can fetch data directly from the Shopify Storefront API using GraphQL queries, without extra middleware libraries.
To minimize load time, use generateStaticParams(). It lets you generate static HTML paths for your most popular products at build time. Less frequently visited products are rendered dynamically on first request and cached.
Here is an example of a server component fetching product data from Shopify based on the handle parameter:
import { notFound } from "next/navigation";
interface ProductProps {
params: { handle: string };
}
async function getShopifyProduct(handle: string) {
const res = await fetch("https://your-shop-name.myshopify.com/api/2024-04/graphql.json", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Shopify-Storefront-Access-Token": process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN || "",
},
body: JSON.stringify({
query: `query getProduct($handle: String!) { product(handle: $handle) { title description } }`,
variables: { handle },
}),
next: { revalidate: 3600 }
});
const { data } = await res.json();
return data?.product;
}
export default async function ShopifyProductPage({ params }: ProductProps) {
const product = await getShopifyProduct(params.handle);
if (!product) {
notFound();
}
return (
<main className="product-detail">
<h1>{product.title}</h1>
<p>{product.description}</p>
</main>
);
}
Precisely combining Next.js's flexible routing with Shopify's efficient API lets you build a store that loads almost instantly. If you are planning a headless sales platform, professional Shopify store implementation with Next.js helps you get the full value from both technologies.
FAQ
Can I still use the Pages Router alongside the App Router in the same project?
Yes. Next.js fully supports both routers in one application, which enables gradual, safe migration of older projects to the new standard. If route names conflict, App Router routes always take precedence.
How does prefetching work in the Link component, and can it be disabled?
Prefetching automatically fetches related route code and data in the background when a Link component enters the viewport. It is enabled by default in production. You can turn it off by setting prefetch={false} on the Link component.
What is the difference between a catch-all route and an optional catch-all route?
The difference is how the base path is handled. A classic catch-all ([...slug]) requires at least one parameter in the URL to match. An optional catch-all ([[...slug]]) also matches a path with no parameters, which is ideal for category filters, for example.
How do I handle a 404 for a product that does not exist in Shopify?
If the Shopify API query returns no data for the given handle, call notFound() from next/navigation inside the component. That stops rendering immediately and shows the nearest not-found.tsx file.
Can layout.js and template.js coexist in the same folder?
Yes. They can live in the same route segment. In that case, Next.js renders the template inside the layout—the layout wraps the template, and the template wraps the actual page component.
How does error handling in error.js differ from global-error.js?
error.js handles errors within a specific route segment and its children. It cannot catch errors in the root global layout (src/app/layout.tsx). The dedicated global-error.js file handles errors at the top level of the application.