The App Router and file-based routing
In Next.js, your folder structure is your routing. There is no route
configuration file — you create a folder, add a page.tsx, and that URL exists.
Folders are URLs
src/app/
page.tsx → /
about/page.tsx → /about
products/page.tsx → /products
products/[slug]/page.tsx → /products/atta-5kg
cart/page.tsx → /cart
page.tsx is what makes a folder a route. A folder without one is not
reachable — which is useful, because it lets you group files without creating
URLs.
// src/app/products/page.tsx
export default function ProductsPage() {
return <h1>All products</h1>;
}
A default export. Next.js finds it by filename, so the function's name is for you rather than the framework.
Dynamic segments
Square brackets capture part of the URL:
// src/app/products/[slug]/page.tsx
type Props = { params: Promise<{ slug: string }> };
export default async function ProductPage({ params }: Props) {
const { slug } = await params;
return <h1>Product: {slug}</h1>;
}
/products/atta-5kg gives slug === "atta-5kg".
params is a promise and must be awaited. This changed in Next.js 15;
older tutorials destructure it directly and that is now wrong. If you see
params.slug used without await, the example predates the change.
Catch-all segments exist too — [...slug] matches any depth, [[...slug]]
makes it optional. You will rarely need them outside documentation sites.
Query strings
type Props = {
searchParams: Promise<{ category?: string; page?: string }>;
};
export default async function ProductsPage({ searchParams }: Props) {
const { category, page = "1" } = await searchParams;
return <p>Category: {category ?? "all"}, page {page}</p>;
}
Also a promise. Every value is string | string[] | undefined, because
?tag=a&tag=b is legal — convert deliberately, and remember Number("") is
0.
Use the URL for state that should survive a refresh or be shareable. Filters,
search terms, pagination. A user copying the address bar should get the same
results back, and that is free if the state lives in the URL rather than in
useState.
The special files
Each is optional and applies to its folder and everything below:
| File | Does |
|---|---|
page.tsx |
the route's content |
layout.tsx |
wraps this route and its children |
loading.tsx |
shown while the page is loading |
error.tsx |
shown when it throws |
not-found.tsx |
shown for notFound() or unmatched routes |
route.ts |
an API endpoint rather than a page |
layout.tsx, loading.tsx and error.tsx are the next lesson but one.
The root layout is required:
// src/app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
That <html> and <body> exist only here. Page components render inside.
Organising without creating URLs
Two mechanisms worth knowing.
Route groups — a folder in (brackets) is ignored in the URL:
src/app/
(shop)/
layout.tsx
products/page.tsx → /products
cart/page.tsx → /cart
(admin)/
layout.tsx
dashboard/page.tsx → /dashboard
Two different layouts, no /shop or /admin in the URL. This is how you give
the shop and the admin area separate chrome.
Private folders — a folder starting with _ is never routed:
src/app/products/_components/product-card.tsx
Useful for keeping a route's components next to it. src/components/ for
anything shared is equally fine; pick one convention.
Linking
import Link from "next/link";
<Link href="/products">All products</Link>
<Link href={`/products/${product.slug}`}>{product.name}</Link>
Use Link, not <a>, for internal navigation. An <a> does a full page
load: everything downloads again and all client state is lost. Link fetches
only what changed and swaps it in.
Link also prefetches pages in the viewport, so by the time somebody taps, the
next page is often already there. That is most of why a Next.js site feels fast.
External links use <a> normally.
Navigating in code
"use client";
import { useRouter } from "next/navigation";
export function CheckoutButton() {
const router = useRouter();
return <button onClick={() => router.push("/checkout")}>Checkout</button>;
}
next/navigation, not next/router — the latter is the old Pages Router and
its imports fail here with a confusing error.
router.push() adds to history; router.replace() does not, which is right
after a login so Back does not return to the form. router.refresh() re-fetches
the current route's server data without losing client state, which matters after
a mutation.
Reading the current URL:
"use client";
import { usePathname, useSearchParams } from "next/navigation";
All three of these hooks require "use client".
API routes
A route.ts makes an endpoint instead of a page:
// src/app/api/health/route.ts
export async function GET() {
return Response.json({ status: "ok" });
}
Export a function named after the HTTP method. Available at /api/health.
In this course most API work lives in NestJS, so these are for things that
belong to the front end — webhooks, health checks, an occasional proxy. A folder
cannot have both page.tsx and route.ts.
Check your work
What makes a folder a route: a page.tsx. A folder without one is not
reachable, which is how you group files without creating URLs.
Why params must be awaited: it is a promise in Next.js 15. Destructuring
it directly is the older API.
Reading a query string: const { category, page = "1" } = await searchParams
— every value is string | string[] | undefined.
Why filters belong in the URL: they survive a refresh, work with Back, and can be shared.
What a route group does: a folder in (brackets) is omitted from the URL,
so you can give sections different layouts without changing paths.
Why Link rather than <a>: an <a> does a full page load, discarding all
client state. Link fetches only what changed and prefetches what is visible.
push versus replace: replace does not add a history entry, which is
what you want after a login so Back does not return to the form.
Which import for navigation: next/navigation. next/router is the old
Pages Router.
Practice
- Create
/products,/cartand/about. Confirm each renders. - Add
/products/[slug]and read the slug. Visit two different products. - Destructure
paramswithout awaiting it and read the error. - Read
?category=grains&page=2fromsearchParamsand render both, handling the missing case. - Group
/productsand/cartunder a(shop)route group with a shared layout. Confirm the URLs do not change. - Add a
_componentsfolder inside a route and confirm it is not routable. - Replace an internal
<a>withLinkand compare the Network tab for each. - Add a button that navigates with
router.push. Then usereplaceand compare the Back button's behaviour. - Create
/api/healthreturning JSON and call it with curl.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship