Next.js Tutorial for Beginners: App Router in 1 Project
Short answer: in the Next.js App Router, every folder inside app/ is a part of the URL, and a file called page.tsx makes that URL a real page. app/page.tsx is your home page, app/about/page.tsx is /about, and layout.tsx wraps the pages below it with shared parts like a navbar. Create a project with npx create-next-app@latest, accept the recommended defaults, load data inside async server components, add "use client" only to the parts people click, run npm run build, and deploy. That is the whole beginner path, and this guide walks you through it with one real project: a portfolio site for mouhimen.dev.
Watch the video (about 10 minutes): https://youtu.be/an538ffEdQI
How I checked this. Everything here was tested on 9 November 2026 with Next.js 16.4.5, the latest stable version on npm that day. I created the project with create-next-app, built every file below, and ran next build. The four error messages in this guide are copied from that run, not from memory. The facts come from the official docs that ship inside the package (node_modules/next/dist/docs/).
What you need
- Node.js 20.9 or newer (the minimum for Next.js 16). Check with
node -v. - A code editor (VS Code, Cursor, or anything you like).
- A little HTML and JavaScript. Some React helps, but you can follow without it.
- For the deploy step: a free GitHub account and a Vercel account.
Why Next.js instead of plain React?
Plain React gives you components. You still have to pick a router, a way to load data, and a build setup yourself. Next.js gives you all of that in one framework, and it renders your pages on the server first, so they load fast and search engines like Google can read them. The App Router (the app/ folder) is the way the Next.js docs recommend building new apps today.
Step 1: Create the app
Open a terminal and run:
npx create-next-app@latest mouhimen-portfolio
It asks: Would you like to use the recommended Next.js defaults? Choose Yes, use recommended defaults. In Next.js 16.4 that gives you TypeScript, ESLint, Tailwind CSS, the App Router, no src/ folder, Cache Components, and a file called AGENTS.md (more on that one at the end).
Start the dev server:
cd mouhimen-portfolio
npm run dev
Open http://localhost:3000 and you’ll see the starter page. Next.js 16 uses Turbopack by default, so it starts in about a second.
The one idea behind routes
| File | URL |
|---|---|
app/page.tsx | / |
app/about/page.tsx | /about |
app/projects/page.tsx | /projects |
app/projects/[slug]/page.tsx | /projects/anything |
app/layout.tsx | wraps every page |
A folder alone is not a page. The route becomes public only when a page.tsx (or a route.ts for APIs) exists inside it.
Step 2: Pages, a layout and links
Replace the starter code in app/page.tsx with a simple hero:
import Link from "next/link";
export default function Home() {
return (
<main className="mx-auto max-w-3xl px-8 py-24">
<h1 className="text-5xl font-bold">Hi, I'm Abd almouhimen Shamout</h1>
<p className="mt-4 text-xl text-zinc-600">I build Laravel APIs and Next.js apps.</p>
<Link href="/projects" className="mt-8 inline-block rounded-lg bg-teal-600 px-6 py-3 text-white">
See my projects
</Link>
</main>
);
}
The class names are Tailwind CSS, which create-next-app already set up.
Now add an about page. Create the folder app/about and a page.tsx inside it:
export default function AboutPage() {
return <h1 className="text-4xl font-bold">About me</h1>;
}
Save, and /about works. No router config and no extra package.
Every page needs the same navbar, so make one component in components/Navbar.tsx. Use the Link component from next/link, not a plain <a> tag: Link moves between pages without a full page reload, and Next.js prefetches the page when the link scrolls into view.
import Link from "next/link";
export default function Navbar() {
return (
<nav className="flex gap-6 px-8 py-4 border-b">
<Link href="/" className="font-bold">mouhimen.dev</Link>
<Link href="/projects">Projects</Link>
<Link href="/about">About</Link>
</nav>
);
}
Then add it to the root layout, app/layout.tsx. The root layout is required and must keep the <html> and <body> tags. While you’re there, change the metadata title, which becomes the browser tab title and the title Google shows:
import type { Metadata } from "next";
import "./globals.css";
import Navbar from "@/components/Navbar";
export const metadata: Metadata = {
title: "Abd almouhimen Shamout | mouhimen.dev",
description: "Full stack developer: Laravel APIs and Next.js apps.",
};
export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html lang="en">
<body>
<Navbar />
{children}
</body>
</html>
);
}
(create-next-app also adds Geist fonts to this file. You can keep them.)
Step 3: Load data in a server component
This is the biggest change from plain React. In the App Router, every component is a server component by default. A page can be an async function and await its data directly. No useEffect, no loading state, no extra data library.
Keep the data in lib/projects.ts. In a real app this function would query your database or call an API with fetch(); the page code stays the same.
import { cacheLife } from "next/cache";
export type Project = { slug: string; title: string; stack: string[]; summary: string };
const projects: Project[] = [
{ slug: "helpdesk-api", title: "Help Desk REST API", stack: ["Laravel 13", "Sanctum"], summary: "Ticket API for Abd almouhimen Shamout's support team." },
{ slug: "ticket-board", title: "Ticket Board", stack: ["Next.js", "Tailwind"], summary: "Drag-and-drop board for open tickets." },
{ slug: "take-away-supports", title: "Take Away Supports", stack: ["Remotion", "React"], summary: "Motion-design videos for the YouTube channel." },
];
export async function getProjects() {
"use cache";
cacheLife("hours");
return projects;
}
export async function getProject(slug: string) {
"use cache";
cacheLife("hours");
return projects.find((p) => p.slug === slug);
}
What’s new in Next.js 16: the recommended defaults turn on Cache Components (cacheComponents: true in next.config.ts). With it, data is not cached unless you ask. The "use cache" directive plus cacheLife("hours") tells Next.js to build this result once, serve it instantly, and refresh it at most every hour.
The projects page, app/projects/page.tsx:
import Link from "next/link";
import { getProjects } from "@/lib/projects";
import LikeButton from "@/components/LikeButton";
export default async function ProjectsPage() {
const projects = await getProjects();
return (
<main className="mx-auto max-w-4xl px-8 py-16">
<h1 className="text-4xl font-bold">Projects</h1>
<div className="mt-8 grid gap-6 sm:grid-cols-3">
{projects.map((p) => (
<div key={p.slug} className="rounded-xl border p-5">
<Link href={`/projects/${p.slug}`} className="text-xl font-semibold">{p.title}</Link>
<p className="mt-2 text-sm text-zinc-500">{p.stack.join(" · ")}</p>
<div className="mt-4"><LikeButton /></div>
</div>
))}
</div>
</main>
);
}
Client components: only where people click
A like button with a counter needs useState, and state only runs in the browser. Put it in its own file with "use client" on the very first line:
"use client";
import { useState } from "react";
export default function LikeButton() {
const [likes, setLikes] = useState(0);
return (
<button onClick={() => setLikes(likes + 1)} className="rounded-full bg-teal-600 px-4 py-2 text-white">
♥ {likes}
</button>
);
}
Keep client components small, like islands. The rest of the page stays on the server.
Step 4: One page for every project (dynamic route)
Create app/projects/[slug]/page.tsx. The square brackets make it a dynamic segment, so /projects/helpdesk-api, /projects/ticket-board and any other slug all use this one file.
import { notFound } from "next/navigation";
import { getProject, getProjects } from "@/lib/projects";
export async function generateStaticParams() {
const projects = await getProjects();
return projects.map((p) => ({ slug: p.slug }));
}
export default async function ProjectPage({ params }: PageProps<"/projects/[slug]">) {
const { slug } = await params;
const project = await getProject(slug);
if (!project) notFound();
return (
<main className="mx-auto max-w-3xl px-8 py-16">
<h1 className="text-4xl font-bold">{project.title}</h1>
<p className="mt-4 text-lg">{project.summary}</p>
</main>
);
}
Three things to notice:
paramsis a Promise (since Next.js 15), so youawaitit.notFound()shows a proper 404 page for unknown slugs.generateStaticParamsreturns the list of slugs, so Next.js builds every project page ahead of time. With Cache Components on, it also prevents a build error (error 2 below).
PageProps<"/projects/[slug]"> and LayoutProps<"/"> are type helpers Next.js generates for you, so you don’t need to write the prop types by hand.
Step 5: Put it live
Always build on your own machine first:
npm run build
It prints every route. A circle (○) means a static page, ◐ means partial prerender, and the cached pages show their revalidate time (here 1h, from cacheLife("hours")). If something is wrong, you find out now, not after you deploy.
Then:
- Push the project to a GitHub repository.
- On Vercel, choose Add New Project and Import the repository.
- Press Deploy. Vercel detects Next.js by itself, and about a minute later you have a live link. Every
git pushdeploys again.
Prefer your own server? Next.js runs anywhere Node.js runs:
npm run build
npm start
Errors and fixes
1. “You’re importing a module that depends on useState into a React Server Component module.”
You used a hook (useState, useEffect, onClick handlers) in a file without "use client". Fix: add "use client"; as the very first line of that file, above the imports. Only that component and its children become client code.
2. “Next.js encountered uncached or runtime data during prerendering.”
This appears in npm run build when something awaits data (fetch, a database call, params, cookies()) outside a <Suspense> boundary. The message lists the fixes:
- Stream it: wrap that part in
<Suspense fallback={<p>Loading...</p>}>. - Cache it: add
"use cache"to the data function. - On a
[slug]page, addgenerateStaticParams, because then the slugs are known at build time. This is exactly what happened to my[slug]page before I added it.
3. “Property ‘slug’ does not exist on type ‘Promise<{ slug: string; }>’.”
Old tutorials write params.slug. Since Next.js 15, params is a Promise. Fix: const { slug } = await params;
4. The page is a 404, but there is no error at all
You renamed projects to _projects to “tidy up”. A folder that starts with an underscore is a private folder: Next.js skips it for routing on purpose, so /projects becomes a 404 and the terminal stays silent. Folders in round brackets, like (marketing), are route groups and are left out of the URL too. Fix: check the folder name first, then check that the file is named exactly page.tsx, all lowercase.
Bonus: AGENTS.md keeps AI coding tools up to date
create-next-app adds an AGENTS.md file to the project root. It tells AI coding assistants that this Next.js version has breaking changes and that they should read the docs bundled at node_modules/next/dist/docs/ before writing code. In my own projects this fixes the most common problem with AI help: it writes old Next.js code from its training (like params.slug). Keep the file, and if your tool doesn’t read it by itself, tell it to read AGENTS.md first.
FAQ
Is Next.js good for beginners?
Yes, if you know basic HTML and JavaScript. The App Router’s “folder = route” rule is easy to learn, and you don’t have to set up routing or a build tool yourself.
Should I learn the App Router or the Pages Router?
The App Router. The Next.js docs recommend it for new apps, and new features land there first. The Pages Router still works for older projects.
What is the difference between a server component and a client component?
Server components run on the server, can be async, and can load data directly. Client components start with "use client", run in the browser, and are needed for state, effects and click handlers.
Do I need Tailwind CSS to use Next.js?
No. It’s part of the recommended defaults, but you can say no and use plain CSS or CSS Modules.
Is Vercel the only place I can deploy Next.js?
No. Next.js runs on any host that supports Node.js (npm run build then npm start), in a Docker container, or as a static export with fewer features.
Keep going
- Next.js security: Next.js Critical RCE: Patch Now
- Build a Next.js + Tailwind app with AI agents: Google Antigravity Tutorial for Beginners
- The back end for a site like this: Laravel API Tutorial for Beginners
Next video: Laravel Sanctum API Authentication, where we add register, login and logout to a Laravel API.
Leave a Reply