RizTech Academy logo
RizTech Academy
Next.js: Routing and RenderingLesson 1 of 630 min

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.

"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

  1. Create /products, /cart and /about. Confirm each renders.
  2. Add /products/[slug] and read the slug. Visit two different products.
  3. Destructure params without awaiting it and read the error.
  4. Read ?category=grains&page=2 from searchParams and render both, handling the missing case.
  5. Group /products and /cart under a (shop) route group with a shared layout. Confirm the URLs do not change.
  6. Add a _components folder inside a route and confirm it is not routable.
  7. Replace an internal <a> with Link and compare the Network tab for each.
  8. Add a button that navigates with router.push. Then use replace and compare the Back button's behaviour.
  9. Create /api/health returning 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