0% found this document useful (0 votes)
6 views58 pages

MHR NextJS Comprehensive Notes

The document provides comprehensive training notes for a Next.js internship program, covering topics from basic setup to advanced deployment techniques. It includes a structured table of contents with 15 chapters, detailing essential concepts such as routing, server components, API routes, and authentication. The notes emphasize practical coding examples and best practices to guide learners through building production-ready applications using Next.js 15.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
6 views58 pages

MHR NextJS Comprehensive Notes

The document provides comprehensive training notes for a Next.js internship program, covering topics from basic setup to advanced deployment techniques. It includes a structured table of contents with 15 chapters, detailing essential concepts such as routing, server components, API routes, and authentication. The notes emphasize practical coding examples and best practices to guide learners through building production-ready applications using Next.js 15.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

MHR Internship | Next.

js: From Zero to Production

MASTERY HUB OF RWANDA


MHR
Comprehensive Training Notes

[Link]
From Zero to Production
─────────────────────────────
15 Chapters • 200+ Code Examples • Beginner to Advanced
Routing • Server Components • API Routes • Auth • Prisma • Deployment
Software Development Internship 2025 | Kigali, Rwanda

How to Use These Notes


These notes cover [Link] 15 App Router from absolute beginner to production deployment.
Chapters build on each other — work through them in order on your first pass. Each chapter
includes full working code examples, tip boxes (green), warning boxes (yellow), and error boxes
(red) to help you avoid common mistakes.
Dark boxes = code to type and run • Green = best practices • Yellow = cautions • Red = never do
this • Blue = useful context • Purple = pro tips

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 1 of 58


MHR Internship | [Link]: From Zero to Production

TABLE OF CONTENTS

01 Introduction to [Link] What it is, why it exists, rendering modes

02 Project Setup & File create-next-app, all files explained


Structure
03 File-Based Routing Pages, dynamic routes, special files, layouts

04 Server & Client The most important concept in modern [Link]


Components
05 Data Fetching SSR, SSG, ISR, parallel fetching, error handling

06 API Routes Building a full backend — GET, POST, PUT, DELETE

07 Server Actions Mutate data without API endpoints

08 Middleware Auth guards, redirects, rewrites

09 Styling in [Link] CSS Modules, Tailwind, custom fonts

10 Authentication JWT, HTTP-only cookies, register/login/logout

11 Images, Metadata & SEO next/image, Open Graph, sitemap

12 Database with Prisma Schema, CRUD, relations, pagination

13 Performance & Streaming, Suspense, bundle splitting


Optimisation
14 Deployment Vercel, Andasy, Docker, custom domains

15 Capstone Project Full MHR Learning Platform — spec to deployment

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 2 of 58


MHR Internship | [Link]: From Zero to Production

CH
Introduction to [Link]
01 What it is, why it exists, and how it changes everything

1.1 What is [Link]?


[Link] is an open-source React framework created and maintained by Vercel. While React is a
library focused purely on building user interfaces, [Link] is a full framework that sits on top of
React and solves all the hard problems that React alone leaves to you: routing, server-side
rendering, data fetching, image optimisation, API creation, and production deployment.
In simple terms: React tells you HOW to build a component. [Link] tells you HOW to build a
complete, production-ready web application.

Real-World Analogy
Think of React as a powerful engine. [Link] is the complete car — engine included — with a
steering wheel, gearbox, brakes, lights, and a GPS already built in. You can drive immediately
without assembling the parts yourself.

Brief History
• 2016: [Link] 1.0 released by Vercel (then Zeit)
• 2019: [Link] 9 — API routes introduced
• 2021: [Link] 12 — Rust-based compiler (10x faster builds)
• 2022: [Link] 13 — App Router introduced (beta) — a complete paradigm shift
• 2023: [Link] 14 — App Router stable, Server Actions introduced
• 2024/2025: [Link] 15 — React 19 support, improved caching, turbopack default

Which version will we use?


These notes cover [Link] 15 with the App Router — the current modern standard. Everything you
learn here applies to [Link] 13, 14, and 15. The App Router replaced the old Pages Router which
you may still encounter in older projects.

1.2 Why Use [Link] Over Plain React?


When you build a React app with Vite, everything runs in the browser. The server sends a nearly
empty HTML file, and JavaScript builds the entire page in the user's browser. This is called Client-
Side Rendering (CSR). It works fine for simple apps, but has serious problems at scale:

Problem with plain React How [Link] solves it

SEO is poor: search engines see an empty Server-renders HTML before sending — bots
page see full content
Slow first load: browser must download + run all Pre-renders pages to HTML — page shows
JS first instantly
No built-in routing: must install React Router File-based routing built in — create a file, get a
manually route

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 3 of 58


MHR Internship | [Link]: From Zero to Production

Problem with plain React How [Link] solves it

No backend: need a separate Node/Express API Routes built in — write backend code in the
server for APIs same project
Images need manual optimisation next/image auto-resizes, compresses, and lazy-
loads images
Environment variables require manual setup Built-in .env support with server/client
separation

1.3 How [Link] Renders Pages — The Big Picture


Understanding rendering is the single most important concept in [Link]. There are four main
rendering strategies, and choosing the right one for each page is what separates good [Link]
developers from great ones.

Strategy Acronym When HTML is built Best for

Client-Side Rendering CSR In the browser, on User dashboards,


every visit real-time data
Server-Side SSR On the server, on User-specific pages,
Rendering every request live data
Static Site Generation SSG Once at build time Blog posts, docs,
marketing pages
Incremental Static ISR At build time + Product pages, news,
Regen. refreshed every N sec prices

Don't worry if this is unclear now


We will revisit each strategy in depth with code examples in Chapter 6. For now, just know that
[Link] supports all four — and you choose per page.

1.4 The App Router vs The Pages Router


[Link] has two routing systems. If you have seen older [Link] tutorials, they likely use the Pages
Router (where pages live in a pages/ folder). Since [Link] 13, the App Router is the
recommended system and what you should learn today.

Feature Pages Router (old) App Router (current)

Folder pages/ app/


Default component type Client Component Server Component
Layouts Manual _app.js + custom logic [Link] — automatic nesting
Data fetching getServerSideProps, async/await directly in
getStaticProps components
Streaming / Suspense Not supported Built-in
Learn this? Only if maintaining old projects YES — this is the future

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 4 of 58


MHR Internship | [Link]: From Zero to Production

If you see getServerSideProps or getStaticProps in a tutorial


That is the old Pages Router. It still works but is not what you should write in new projects. These
notes cover the App Router exclusively.

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 5 of 58


MHR Internship | [Link]: From Zero to Production

CH
Project Setup & File Structure
02 Creating your first [Link] app and understanding every file

2.1 Prerequisites
Before creating a [Link] project, make sure these are installed on your machine:
• [Link] version 18.17 or higher (LTS recommended)
• npm version 9+ (comes with [Link])
• VS Code — recommended editor
• Git — for version control

# Verify your versions


node --version # Should be v18.x.x or higher
npm --version # Should be 9.x.x or higher

2.2 Creating a [Link] 15 Project


Use the official create-next-app tool. It sets up everything automatically — TypeScript config,
ESLint, Tailwind CSS, the app/ folder, and more.

npx create-next-app@latest my-nextjs-app

You will be asked a series of questions. Here are the recommended answers for this course:

Question Recommended Answer Reason

Would you like to use No We use JavaScript for


TypeScript? simplicity
Would you like to use ESLint? Yes Catches errors while you code
Would you like to use Tailwind Yes Fast, utility-first styling
CSS?
Would you like your code Yes Keeps project organised
inside a src/ directory?
Would you like to use App Yes This is what we are learning
Router?
Would you like to use Yes Much faster dev server
Turbopack for next dev?
Would you like to customise No Default @/ alias is fine
the import alias?

cd my-nextjs-app
npm run dev

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 6 of 58


MHR Internship | [Link]: From Zero to Production

# Open your browser at: [Link]


# You should see the [Link] welcome page

2.3 Full Project Structure Explained


After creation, your project looks like this. Let's go through every file and folder:

my-nextjs-app/
├── src/
│ └── app/
│ ├── [Link] # Browser tab icon
│ ├── [Link] # Global CSS applied to all pages
│ ├── [Link] # ROOT LAYOUT — wraps every page
│ └── [Link] # HOME PAGE — rendered at /
├── public/ # Static files served as-is
│ ├── [Link]
│ └── [Link]
├── .[Link] # Environment variables (create this
yourself)
├── .gitignore # Files Git should not track
├── [Link] # ESLint rules
├── [Link] # JS path aliases (@/ = src/)
├── [Link] # [Link] configuration
├── [Link] # Dependencies and scripts
├── [Link] # Exact dependency versions (do not edit)
└── [Link] # Tailwind configuration

Key Files Explained


src/app/[Link] — The Root Layout
This is the most important file. It wraps every page in your application. Any HTML you put here
(NavBar, Footer, providers) will appear on every page automatically.
// src/app/[Link]
import './[Link]';

export const metadata = {


title: 'My [Link] App',
description: 'Built with [Link] 15',
};

export default function RootLayout({ children }) {


return (
<html lang='en'>
<body>
{/* children = the current page being visited */}
{children}
</body>
</html>
);
}

src/app/[Link] — The Home Page


This is your home page. It renders when a user visits /. Delete the default content and replace it
with your own.
© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 7 of 58
MHR Internship | [Link]: From Zero to Production

// src/app/[Link]
export default function HomePage() {
return (
<main>
<h1>Welcome to My App</h1>
<p>Built with [Link] 15</p>
</main>
);
}

[Link] — [Link] Configuration


// [Link] — you rarely need to change this
/** @type {import('next').NextConfig} */
const nextConfig = {
// Allow images from external domains
images: {
domains: ['[Link]', '[Link]'],
},
// Experimental features
experimental: {
serverActions: { allowedOrigins: ['localhost:3000'] },
},
};

export default nextConfig;

.[Link] — Environment Variables


# .[Link] — NEVER commit this to GitHub
# Create this file yourself in the project root

# Available only on the SERVER (safe for secrets)


DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_SECRET=my-super-secret-key-12345
STRIPE_SECRET_KEY=sk_test_xxxxx

# Available on BOTH server and browser (prefix with NEXT_PUBLIC_)


NEXT_PUBLIC_APP_NAME=My [Link] App
NEXT_PUBLIC_API_URL=[Link]

NEVER put secrets in NEXT_PUBLIC_ variables


Any variable starting with NEXT_PUBLIC_ is sent to the browser and visible to anyone. Only use
NEXT_PUBLIC_ for non-sensitive values like your app name or public API base URL. Keep API
keys, database passwords, and JWT secrets in non-prefixed variables.

2.4 npm Scripts — Commands You'll Use Every Day


Command What it does

npm run dev Start development server at localhost:3000 with


hot reload
npm run build Build the app for production (run before
deploying)

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 8 of 58


MHR Internship | [Link]: From Zero to Production

Command What it does

npm run start Start the production server (after npm run build)
npm run lint Check code for ESLint errors

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 9 of 58


MHR Internship | [Link]: From Zero to Production

CH
File-Based Routing
03 How folders and files become URLs automatically

3.1 How Routing Works in [Link]


In React with React Router, you manually configure every route in a Routes component. [Link]
eliminates this completely. Every folder you create inside src/app/ automatically becomes a URL
segment, and every [Link] file inside that folder becomes the page rendered at that URL.

The Rule
Folder name = URL segment. [Link] inside that folder = the page rendered at that URL. That is
the entire routing system.

src/app/
├── [Link] → /
├── about/
│ └── [Link] → /about
├── courses/
│ ├── [Link] → /courses
│ └── react/
│ └── [Link] → /courses/react
├── blog/
│ ├── [Link] → /blog
│ └── [slug]/
│ └── [Link] → /blog/any-post-title
└── dashboard/
├── [Link] → /dashboard
└── settings/
└── [Link] → /dashboard/settings

3.2 Creating Your First Pages


Let's create a multi-page site step by step.

Step 1 — Home Page (already exists)


// src/app/[Link]
export default function HomePage() {
return (
<div>
<h1>MHR Learning Platform</h1>
<p>Welcome to Mastery Hub of Rwanda</p>
</div>
);
}

Step 2 — About Page

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 10 of 58


MHR Internship | [Link]: From Zero to Production

// src/app/about/[Link]
// Create the folder src/app/about/ and add [Link]

export default function AboutPage() {


return (
<div>
<h1>About MHR</h1>
<p>Mastery Hub of Rwanda empowers the next generation of tech
leaders.</p>
</div>
);
}

// Visit: [Link]

Step 3 — Courses Page


// src/app/courses/[Link]
export default function CoursesPage() {
return (
<div>
<h1>Our Courses</h1>
<ul>
<li>[Link]</li>
<li>[Link]</li>
<li>React Native</li>
<li>Cloud with Andasy</li>
</ul>
</div>
);
}

// Visit: [Link]

3.3 Dynamic Routes — URLs with Variables


Dynamic routes let a single [Link] handle many different URLs. For example, one component to
show the detail of any course — /courses/react, /courses/nextjs, /courses/react-native.
To create a dynamic segment, name the folder with square brackets: [paramName]

// Folder structure:
src/app/courses/[id]/[Link]

// This handles:
// /courses/1
// /courses/nextjs
// /courses/anything-at-all

// src/app/courses/[id]/[Link]

// The 'params' prop contains the dynamic segment value


export default function CourseDetailPage({ params }) {
return (
<div>

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 11 of 58


MHR Internship | [Link]: From Zero to Production

<h1>Course: {[Link]}</h1>
<p>You are viewing course with ID: {[Link]}</p>
</div>
);
}

// /courses/react → [Link] = 'react'


// /courses/42 → [Link] = '42'
// /courses/next-intro → [Link] = 'next-intro'

Catch-All Routes
To capture multiple URL segments (like /docs/intro/getting-started), use [...slug]:
// src/app/docs/[...slug]/[Link]

export default function DocsPage({ params }) {


// [Link] is an ARRAY
// /docs/intro → [Link] = ['intro']
// /docs/intro/setup → [Link] = ['intro', 'setup']
// /docs/a/b/c → [Link] = ['a', 'b', 'c']

return <h1>Docs: {[Link](' / ')}</h1>;


}

3.4 Special Files in the App Router


The App Router has several special file names that serve specific purposes. Understanding these
is crucial.

File Purpose

[Link] The UI for a route. Without this, the route does


not exist
[Link] Shared UI that wraps [Link] and persists
across navigations
[Link] Automatic loading skeleton shown while the
page is loading
[Link] Error boundary — shown when the page throws
an error
[Link] Custom 404 page for this route segment
[Link] API endpoint (no UI) — replaces the old
pages/api/ folder
[Link] Like [Link] but re-renders on every
navigation (rarely used)

[Link] — Automatic Suspense Fallback


// src/app/courses/[Link]
// This shows automatically while courses/[Link] is loading

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 12 of 58


MHR Internship | [Link]: From Zero to Production

export default function CoursesLoading() {


return (
<div>
<div style={{ width: '200px', height: '24px', background: '#E5E7EB',
borderRadius: '4px' }} />
<p>Loading courses...</p>
</div>
);
}

// [Link] automatically wraps [Link] in a React Suspense boundary


// and shows this component while waiting for async data

[Link] — Error Boundaries


// src/app/courses/[Link]
// MUST be a Client Component (errors involve interactivity)
'use client';

export default function CoursesError({ error, reset }) {


return (
<div>
<h2>Something went wrong loading courses!</h2>
<p>{[Link]}</p>
<button onClick={reset}>Try again</button>
</div>
);
}

[Link] — Custom 404 Pages


// src/app/[Link] — global 404 page
import Link from 'next/link';

export default function NotFoundPage() {


return (
<div>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<Link href='/'>Go back home</Link>
</div>
);
}

// You can also trigger it programmatically:


import { notFound } from 'next/navigation';

export default async function CourseDetailPage({ params }) {


const course = await getCourse([Link]);
if (!course) notFound(); // Shows the [Link] page
return <h1>{[Link]}</h1>;
}

3.5 Layouts — Persistent UI Across Pages


© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 13 of 58
MHR Internship | [Link]: From Zero to Production

Layouts are one of the most powerful features of the App Router. A [Link] file wraps all pages
in the same folder and all sub-folders. Layouts persist across navigations — they do NOT re-
render when you navigate between pages, which makes transitions fast and preserves state (like
scroll position).

Nested Layouts Example


src/app/
├── [Link] ← Root layout: wraps EVERYTHING
├── [Link] ← /
├── dashboard/
│ ├── [Link] ← Dashboard layout: wraps all /dashboard/* pages
│ ├── [Link] ← /dashboard
│ ├── settings/
│ │ └── [Link] ← /dashboard/settings (wrapped by BOTH layouts)
│ └── profile/
│ └── [Link] ← /dashboard/profile (wrapped by BOTH layouts)

// src/app/[Link] — Root layout (every page gets this)


import NavBar from '@/components/NavBar';
import Footer from '@/components/Footer';
import './[Link]';

export const metadata = { title: 'MHR Platform' };

export default function RootLayout({ children }) {


return (
<html lang='en'>
<body>
<NavBar />
<main>{children}</main>
<Footer />
</body>
</html>
);
}

// src/app/dashboard/[Link] — Dashboard layout (only dashboard pages)


import DashboardSidebar from '@/components/DashboardSidebar';

export default function DashboardLayout({ children }) {


return (
<div style={{ display: 'flex' }}>
<DashboardSidebar />
<section style={{ flex: 1, padding: '24px' }}>{children}</section>
</div>
);
}

Layouts do not re-render on navigation


When a user navigates from /dashboard to /dashboard/settings, the DashboardLayout component
stays mounted — only the inner page changes. This is great for sidebars, because their scroll
position and state are preserved.

3.6 Navigation — The Link Component and useRouter

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 14 of 58


MHR Internship | [Link]: From Zero to Production

Never use HTML <a> tags for internal navigation in [Link]. Use the built-in Link component,
which prefetches pages on hover for instant transitions.

// src/components/[Link]
import Link from 'next/link';

export default function NavBar() {


return (
<nav>
{/* Link prefetches the destination page on hover */}
<Link href='/'>Home</Link>
<Link href='/courses'>Courses</Link>
<Link href='/about'>About</Link>

{/* Open in a new tab */}


<Link href='[Link] target='_blank' rel='noopener noreferrer'>
MHR Website
</Link>
</nav>
);
}

Programmatic Navigation — useRouter


Use useRouter when you need to navigate from inside a function (e.g., after a form submission).
This requires 'use client'.
'use client';
import { useRouter } from 'next/navigation';

export default function LoginForm() {


const router = useRouter();

async function handleSubmit(formData) {


const result = await loginUser(formData);
if ([Link]) {
[Link]('/dashboard'); // Navigate forward
// [Link]('/dashboard'); // Navigate without adding to history
// [Link](); // Go back one page
// [Link](); // Re-fetch server data
}
}

return <form action={handleSubmit}>...</form>;


}

Reading Route Info — usePathname, useSearchParams


'use client';
import { usePathname, useSearchParams } from 'next/navigation';

export default function ActiveNavLink({ href, children }) {


const pathname = usePathname(); // e.g., '/courses'
const isActive = pathname === href;

return (

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 15 of 58


MHR Internship | [Link]: From Zero to Production

<Link href={href} style={{ fontWeight: isActive ? 'bold' : 'normal' }}>


{children}
</Link>
);
}

// useSearchParams — read URL query string: /search?q=react&page=2


function SearchPage() {
const searchParams = useSearchParams();
const query = [Link]('q'); // 'react'
const page = [Link]('page'); // '2'
return <p>Searching for: {query}</p>;
}

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 16 of 58


MHR Internship | [Link]: From Zero to Production

CH
Server & Client Components
04 The most important mental model in modern [Link]

4.1 The Two Types of Components


The App Router introduces the most important concept in modern React and [Link]: the
distinction between Server Components and Client Components. Understanding this deeply is
what separates beginners from professional [Link] developers.

Server Component Client Component

Where does it run? On the server only On the server (first render) +
browser
Default in App Router? YES — default No — must add 'use client'
Can use useState / useEffect? NO YES
Can access database directly? YES NO
Can access [Link] YES NO (only NEXT_PUBLIC_)
secrets?
Adds to JS bundle? NO — zero JS sent YES — sent to browser
Good for Layouts, data fetching, static Buttons, forms, modals,
content interactive UI

4.2 Server Components — The Default


Every component in app/ is a Server Component by default. They run on the server, can directly
access databases and secrets, and send ready-made HTML to the browser. They do NOT send
JavaScript — which makes pages load faster.

// src/app/courses/[Link]
// No 'use client' = Server Component

// You can use async/await directly — no useEffect needed!


async function getCourses() {
const res = await fetch('[Link] {
cache: 'no-store', // Always fetch fresh data
});
if (![Link]) throw new Error('Failed to fetch courses');
return [Link]();
}

export default async function CoursesPage() {


// Awaiting directly in the component — this is a Server Component superpower
const courses = await getCourses();

return (
<div>
<h1>Our Courses</h1>
{[Link](course => (

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 17 of 58


MHR Internship | [Link]: From Zero to Production

<div key={[Link]}>
<h2>{[Link]}</h2>
<p>{[Link]}</p>
</div>
))}
</div>
);
}

// The browser receives plain HTML — no JS for this component!

The power of Server Components


In old React, you had to fetch data with useEffect, manage loading/error states, and the user saw
a loading spinner while waiting. In Server Components, the page only renders AFTER the data is
ready — users see complete content instantly with no spinner needed.

4.3 Client Components — Adding Interactivity


When you need state, event handlers, browser APIs, or React hooks like useState and useEffect,
you must declare a Client Component with the 'use client' directive as the very first line.

// src/components/[Link]
'use client'; // ← Must be the FIRST line, before imports

import { useState } from 'react';

export default function SearchBar({ onSearch }) {


const [query, setQuery] = useState('');

function handleChange(e) {
const value = [Link];
setQuery(value);
onSearch(value); // Notify parent
}

return (
<input
type='text'
value={query}
onChange={handleChange}
placeholder='Search courses...'
/>
);
}

Things that REQUIRE 'use client'


• useState, useReducer, useContext
• useEffect, useLayoutEffect
• onClick, onChange, onSubmit (event handlers)
• useRouter, usePathname, useSearchParams
• Browser APIs: localStorage, sessionStorage, window, document
• Third-party libraries that use the above

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 18 of 58


MHR Internship | [Link]: From Zero to Production

4.4 The Composition Pattern — Server + Client Together


The key insight is this: a Server Component CAN render a Client Component as a child, but a
Client Component cannot render a Server Component as a direct child. This means you build a
tree where Server Components are at the top and Client Components are at the leaves.

// src/app/courses/[Link] — SERVER Component


import CourseList from '@/components/CourseList'; // Server
import SearchBar from '@/components/SearchBar'; // Client

async function getCourses() {


return await fetch('/api/courses').then(r => [Link]());
}

export default async function CoursesPage() {


const courses = await getCourses(); // Runs on server

return (
<div>
<SearchBar /> {/* Client Component (interactive) */}
<CourseList courses={courses} /> {/* Server Component (data display) */}
</div>
);
}

Best Practice — Push 'use client' to the leaves


Design your component tree so that large layout and data-fetching components are Server
Components, and only the small interactive pieces (buttons, inputs, dropdowns) are Client
Components. This minimises the JavaScript bundle and maximises performance.

Common Mistake — Making everything 'use client'


New developers often add 'use client' to every file out of habit from [Link]. This defeats the
entire purpose of the App Router. Only add it when you truly need interactivity. A static product
card component should be a Server Component.

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 19 of 58


MHR Internship | [Link]: From Zero to Production

CH
Data Fetching
05 Getting data into your app the right way

5.1 The New Way to Fetch Data


In the Pages Router, data fetching used special functions: getServerSideProps (server) and
getStaticProps (build time). These are GONE in the App Router. Instead, you simply use
async/await directly inside Server Components, or use the fetch() API with cache options to
control behaviour.

The Golden Rule


Fetch data as close to where it is used as possible. Instead of fetching everything at the top level
and passing props down, let each Server Component fetch its own data. [Link] automatically
deduplicates identical requests, so you will never accidentally fetch the same data twice.

5.2 The Four Fetching Strategies with Code

Strategy 1 — Server-Side Rendering (Dynamic, fresh on every request)


Use cache: 'no-store' to always fetch the latest data. This page renders on the server for every
request.
// src/app/dashboard/[Link]
// Best for: user dashboards, live data, personalized content

async function getUserStats(userId) {


const res = await fetch(`[Link] {
cache: 'no-store', // Never cache — always fresh
});
return [Link]();
}

export default async function DashboardPage() {


const stats = await getUserStats('user-123');
return (
<div>
<h1>Dashboard</h1>
<p>Total courses enrolled: {[Link]}</p>
<p>Completed: {[Link]}</p>
</div>
);
}

Strategy 2 — Static Generation (Cached at build time)


Use cache: 'force-cache' (or no cache option, as it is the default) to build the page once. Great for
content that rarely changes.
// src/app/about/[Link]
// Best for: marketing pages, documentation, blog posts

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 20 of 58


MHR Internship | [Link]: From Zero to Production

async function getAboutContent() {


const res = await fetch('[Link] {
cache: 'force-cache', // Cache forever (until next build)
});
return [Link]();
}

export default async function AboutPage() {


const content = await getAboutContent();
return <div dangerouslySetInnerHTML={{ __html: [Link] }} />;
}

Strategy 3 — Incremental Static Regeneration (Revalidate every N seconds)


// src/app/courses/[Link]
// Best for: product listings, pricing, news feeds

async function getCourses() {


const res = await fetch('[Link] {
next: { revalidate: 3600 }, // Regenerate at most every 1 hour (3600
seconds)
});
return [Link]();
}

export default async function CoursesPage() {


const courses = await getCourses();
return (
<ul>
{[Link](c => <li key={[Link]}>{[Link]}</li>)}
</ul>
);
}

Strategy 4 — generateStaticParams (Pre-render dynamic routes)


Use this to tell [Link] which dynamic URLs to pre-build at build time. Without this, dynamic routes
are rendered on-demand (SSR).
// src/app/courses/[id]/[Link]

// This function runs at BUILD TIME


// Return all possible values for the [id] segment
export async function generateStaticParams() {
const courses = await fetch('[Link] =>
[Link]());

// Return an array of objects matching the dynamic segment


return [Link](course => ({
id: String([Link]), // Must be a string
}));
// Returns: [{ id: '1' }, { id: '2' }, { id: '3' }, ...]
}

// This function runs for each pre-built page


export default async function CourseDetailPage({ params }) {
const course = await fetch(`[Link] {

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 21 of 58


MHR Internship | [Link]: From Zero to Production

cache: 'force-cache',
}).then(r => [Link]());

return (
<div>
<h1>{[Link]}</h1>
<p>{[Link]}</p>
</div>
);
}

5.3 Fetch Options Summary


Option Behaviour Use when

cache: 'force-cache' Cache result indefinitely Static content that rarely


(default) changes
cache: 'no-store' Never cache — always re- User data, live prices,
fetch dashboards
next: { revalidate: N } Cache for N seconds, then Semi-dynamic content (news,
regenerate products)
next: { tags: ['courses'] } Tag the cache entry for on- CMS content you want to
demand revalidation purge manually

5.4 Parallel Data Fetching


If a page needs data from multiple sources, do NOT await them one by one — that makes them
run sequentially (slow). Use [Link]() to fetch in parallel.

// BAD — sequential fetching (total time = 200ms + 150ms = 350ms)


const course = await getCourse(id); // 200ms
const reviews = await getReviews(id); // 150ms

// GOOD — parallel fetching (total time = max(200ms, 150ms) = 200ms)


const [course, reviews] = await [Link]([
getCourse(id),
getReviews(id),
]);

// Full example:
export default async function CourseDetailPage({ params }) {
const [course, reviews, instructor] = await [Link]([
getCourse([Link]),
getReviews([Link]),
getInstructor([Link]),
]);

return (
<div>
<h1>{[Link]}</h1>
<p>Instructor: {[Link]}</p>
<p>{[Link]} reviews</p>
</div>
);

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 22 of 58


MHR Internship | [Link]: From Zero to Production

5.5 Error Handling in Data Fetching


// Always handle fetch errors explicitly
async function getCourse(id) {
const res = await fetch(`/api/courses/${id}`, { cache: 'no-store' });

// Check HTTP status code


if ([Link] === 404) {
notFound(); // Render the [Link] page
}

if (![Link]) {
// Throwing here will trigger the nearest [Link] boundary
throw new Error(`Failed to fetch course: ${[Link]}`);
}

return [Link]();
}

// Using try/catch for graceful degradation


export default async function CoursePage({ params }) {
try {
const course = await getCourse([Link]);
return <h1>{[Link]}</h1>;
} catch (error) {
return <p>Could not load this course. Please try again later.</p>;
}
}

On-Demand Cache Revalidation


// src/app/api/revalidate/[Link]
// Call this API to purge cached data when content changes
import { revalidateTag, revalidatePath } from 'next/cache';
import { NextResponse } from 'next/server';

export async function POST(request) {


const { secret, tag, path } = await [Link]();

if (secret !== [Link].REVALIDATION_SECRET) {


return [Link]({ error: 'Unauthorized' }, { status: 401 });
}

if (tag) revalidateTag(tag); // Purge by cache tag


if (path) revalidatePath(path); // Purge specific path

return [Link]({ revalidated: true });


}

// Now when your CMS updates a course, it calls:


// POST /api/revalidate with { secret: '...', tag: 'courses' }
// And [Link] will regenerate the courses pages immediately

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 23 of 58


MHR Internship | [Link]: From Zero to Production

CH
API Routes
06 Building a full backend inside your [Link] project

6.1 What Are API Routes?


API Routes let you build backend endpoints directly inside your [Link] project. Instead of
maintaining a separate Express server, you write server-side logic in [Link] files inside the app/
directory. These files run on the server and are never exposed as JavaScript to the browser.
An API route is just a file that exports HTTP handler functions (GET, POST, PUT, PATCH,
DELETE). Each exported function handles requests with that HTTP method.

When to use API Routes


Use them for: form submissions, data mutations, webhook handlers, third-party API proxies (to
hide your API keys), authentication endpoints, and any server-side business logic. For simple data
reads, Server Components can fetch directly — you do not always need an API route.

6.2 Your First API Route


// src/app/api/hello/[Link]
// URL: GET /api/hello

import { NextResponse } from 'next/server';

export async function GET() {


return [Link]({
message: 'Hello from MHR API!',
timestamp: new Date().toISOString(),
});
}

// Test it: open [Link] in your browser

Full CRUD API — Students Resource


Let's build a complete Students API with GET, POST, PUT, and DELETE methods.

GET and POST all students — /api/students


// src/app/api/students/[Link]
import { NextResponse } from 'next/server';

// In-memory store (use a real database in production)


let students = [
{ id: 1, name: 'Alice Uwimana', email: 'alice@[Link]', course: '[Link]',
enrolled: true },
{ id: 2, name: 'Bob Ndayisaba', email: 'bob@[Link]', course: '[Link]',
enrolled: true },
{ id: 3, name: 'Carol Ingabire', email: 'carol@[Link]', course: 'React
Native', enrolled: false },

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 24 of 58


MHR Internship | [Link]: From Zero to Production

];

// GET /api/students
// Supports: /api/students?course=[Link]&enrolled=true
export async function GET(request) {
const { searchParams } = new URL([Link]);
const courseFilter = [Link]('course');
const enrolledFilter = [Link]('enrolled');

let result = students;

if (courseFilter) {
result = [Link](s => [Link] === courseFilter);
}
if (enrolledFilter !== null) {
result = [Link](s => [Link] === (enrolledFilter === 'true'));
}

return [Link](result);
}

// POST /api/students — create a new student


export async function POST(request) {
const body = await [Link]();

// Validate required fields


if (![Link] || ![Link] || ![Link]) {
return [Link](
{ error: 'name, email and course are required' },
{ status: 400 }
);
}

const newStudent = {
id: [Link] + 1,
name: [Link],
email: [Link],
course: [Link],
enrolled: [Link] ?? false,
};

[Link](newStudent);
return [Link](newStudent, { status: 201 });
}

GET, PUT, DELETE by ID — /api/students/[id]


// src/app/api/students/[id]/[Link]
import { NextResponse } from 'next/server';

// GET /api/students/1
export async function GET(request, { params }) {
const id = Number([Link]);
const student = [Link](s => [Link] === id);

if (!student) {
return [Link]({ error: 'Student not found' }, { status: 404 });
}

return [Link](student);

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 25 of 58


MHR Internship | [Link]: From Zero to Production

// PUT /api/students/1 — full update


export async function PUT(request, { params }) {
const id = Number([Link]);
const index = [Link](s => [Link] === id);

if (index === -1) {


return [Link]({ error: 'Student not found' }, { status: 404 });
}

const body = await [Link]();


students[index] = { ...students[index], ...body, id };
return [Link](students[index]);
}

// DELETE /api/students/1
export async function DELETE(request, { params }) {
const id = Number([Link]);
const index = [Link](s => [Link] === id);

if (index === -1) {


return [Link]({ error: 'Student not found' }, { status: 404 });
}

const deleted = students[index];


students = [Link](s => [Link] !== id);
return [Link]({ message: 'Deleted', student: deleted });
}

6.3 Request Helpers


export async function POST(request) {
// Read JSON body
const body = await [Link]();

// Read form data


const formData = await [Link]();
const name = [Link]('name');

// Read URL search params


const { searchParams } = new URL([Link]);
const page = [Link]('page') ?? '1';

// Read headers
const authHeader = [Link]('Authorization');
const contentType = [Link]('Content-Type');

// Read cookies
const cookies = [Link];
const sessionToken = [Link]('session')?.value;

return [Link]({ received: true });


}

Setting Response Headers and Cookies

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 26 of 58


MHR Internship | [Link]: From Zero to Production

export async function GET() {


const response = [Link]({ data: 'example' });

// Set response headers


[Link]('X-Custom-Header', 'my-value');
[Link]('Cache-Control', 'public, max-age=3600');

// Set a cookie
[Link]({
name: 'session',
value: 'abc123',
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
maxAge: 60 * 60 * 24 * 7, // 7 days in seconds
path: '/',
});

return response;
}

// Delete a cookie
export async function DELETE() {
const response = [Link]({ loggedOut: true });
[Link]('session');
return response;
}

6.4 Calling Your API from the Frontend


// src/components/[Link]
'use client';
import { useState } from 'react';

export default function StudentForm() {


const [status, setStatus] = useState('');

async function handleSubmit(e) {


[Link]();
const formData = new FormData([Link]);

const response = await fetch('/api/students', {


method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: [Link]({
name: [Link]('name'),
email: [Link]('email'),
course: [Link]('course'),
}),
});

if ([Link]) {
const student = await [Link]();
setStatus(`Registered: ${[Link]}`);
[Link]();
} else {
const error = await [Link]();
setStatus(`Error: ${[Link]}`);
}
}

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 27 of 58


MHR Internship | [Link]: From Zero to Production

return (
<form onSubmit={handleSubmit}>
<input name='name' placeholder='Full Name' required />
<input name='email' placeholder='Email' type='email' required />
<input name='course' placeholder='Course' required />
<button type='submit'>Register</button>
{status && <p>{status}</p>}
</form>
);
}

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 28 of 58


MHR Internship | [Link]: From Zero to Production

CH
Server Actions
07 Mutate data without writing API endpoints

7.1 What Are Server Actions?


Server Actions are async functions that run on the server but can be called directly from your
components — including Client Components. They were introduced in [Link] 14 and are now the
recommended way to handle form submissions and data mutations.
The key benefit: you do not need to write a separate API endpoint just to handle a form
submission. You write a function with 'use server', pass it to a form, and [Link] handles all the
network communication automatically.

Why Server Actions?


They eliminate the need for POST API routes for form handling. They work without JavaScript
(progressive enhancement). They automatically revalidate cached data. They reduce boilerplate
by 50% compared to the traditional fetch + API route pattern.

7.2 Your First Server Action


// src/app/students/new/[Link]
// This entire file is a Server Component

import { redirect } from 'next/navigation';


import { revalidatePath } from 'next/cache';

// The Server Action — note 'use server' inside the function


async function createStudent(formData) {
'use server'; // ← This directive marks it as a Server Action

// Extract form fields


const name = [Link]('name');
const email = [Link]('email');
const course = [Link]('course');

// Validate
if (!name || !email || !course) {
throw new Error('All fields are required');
}

// Save to database (or call your API)


await fetch('/api/students', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: [Link]({ name, email, course }),
});

// Revalidate the students list page cache


revalidatePath('/students');

// Redirect after success


redirect('/students');

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 29 of 58


MHR Internship | [Link]: From Zero to Production

// The Page — passes the action to the form


export default function NewStudentPage() {
return (
<form action={createStudent}>
<input name='name' placeholder='Full Name' required />
<input name='email' placeholder='Email' type='email' required />
<input name='course' placeholder='Course' required />
<button type='submit'>Register Student</button>
</form>
);
}

7.3 Server Actions in Separate Files


For larger projects, keep Server Actions in dedicated files. Add 'use server' at the top of the file
(not inside each function).
// src/actions/[Link]
'use server'; // ← Marks the entire file as Server Actions

import { revalidatePath } from 'next/cache';


import { redirect } from 'next/navigation';

export async function createStudent(formData) {


const data = {
name: [Link]('name'),
email: [Link]('email'),
course: [Link]('course'),
};

// In production: await [Link]({ data });


await fetch('/api/students', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: [Link](data),
});

revalidatePath('/students');
redirect('/students');
}

export async function deleteStudent(id) {


await fetch(`/api/students/${id}`, { method: 'DELETE' });
revalidatePath('/students');
}

export async function updateStudent(id, formData) {


const updates = [Link]([Link]());
await fetch(`/api/students/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: [Link](updates),
});
revalidatePath('/students');
redirect('/students');
}

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 30 of 58


MHR Internship | [Link]: From Zero to Production

// src/app/students/new/[Link]
import { createStudent } from '@/actions/students';

export default function NewStudentPage() {


return (
<form action={createStudent}>
<input name='name' required />
<input name='email' type='email' required />
<input name='course' required />
<button type='submit'>Register</button>
</form>
);
}

7.4 Server Actions with useFormStatus and useFormState


For a better user experience, show pending states while the action is running and display errors
without a full page reload.
// src/components/[Link]
'use client';
import { useFormStatus } from 'react-dom';

export function SubmitButton() {


const { pending } = useFormStatus();

return (
<button type='submit' disabled={pending}>
{pending ? 'Registering...' : 'Register Student'}
</button>
);
}

// src/app/students/new/[Link]
import { SubmitButton } from '@/components/SubmitButton';
import { createStudent } from '@/actions/students';

export default function NewStudentPage() {


return (
<form action={createStudent}>
<input name='name' placeholder='Full Name' required />
<input name='email' placeholder='Email' type='email' required />
<input name='course' placeholder='Course' required />
<SubmitButton />
</form>
);
}

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 31 of 58


MHR Internship | [Link]: From Zero to Production

CH
Middleware
08 Running code before every request

8.1 What is Middleware?


Middleware is code that runs BEFORE a request reaches your page or API route. It intercepts
every incoming request and lets you: redirect users, rewrite URLs, add response headers, check
authentication, and more. All of this happens at the Edge — close to the user — so it is extremely
fast.

Where does [Link] live?


At the project ROOT, next to [Link]. NOT inside the src/ or app/ folders. It is a single file
that handles all middleware logic for the entire project.

8.2 Creating [Link]


// [Link] (at the project root)
import { NextResponse } from 'next/server';

export function middleware(request) {


// Log every request (development only)
[Link]('Request:', [Link], [Link]);

// Allow the request to continue


return [Link]();
}

// Restrict middleware to specific paths


export const config = {
matcher: ['/((?!_next/static|_next/image|[Link]).*)'],
// This runs on ALL paths EXCEPT [Link] internal files
};

8.3 Authentication Middleware — The Most Common Use Case


The most important use of middleware is protecting routes. Check for an authentication token on
every request to a protected route, and redirect to the login page if it is missing.
// [Link]
import { NextResponse } from 'next/server';

// Pages that require authentication


const PROTECTED_ROUTES = ['/dashboard', '/profile', '/admin'];

// Pages only for unauthenticated users (redirect if already logged in)


const AUTH_ROUTES = ['/login', '/register'];

export function middleware(request) {


const { pathname } = [Link];

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 32 of 58


MHR Internship | [Link]: From Zero to Production

const token = [Link]('auth-token')?.value;

const isProtected = PROTECTED_ROUTES.some(route =>


[Link](route));
const isAuthRoute = AUTH_ROUTES.some(route => [Link](route));

// Redirect unauthenticated users away from protected pages


if (isProtected && !token) {
const loginUrl = new URL('/login', [Link]);
[Link]('redirect', pathname); // Remember where they
came from
return [Link](loginUrl);
}

// Redirect authenticated users away from login/register pages


if (isAuthRoute && token) {
return [Link](new URL('/dashboard', [Link]));
}

return [Link]();
}

export const config = {


matcher: ['/((?!api|_next/static|_next/image|[Link]).*)'],
};

8.4 URL Rewrites and Redirects


// [Link]
export function middleware(request) {
const { pathname } = [Link];

// REDIRECT: Change the URL the user sees


if (pathname === '/old-courses') {
return [Link](new URL('/courses', [Link]));
}

// REWRITE: Serve different content WITHOUT changing the URL


// User sees /blog/my-post but [Link] serves /posts/my-post
if ([Link]('/blog/')) {
const slug = [Link]('/blog/', '');
return [Link](new URL(`/posts/${slug}`, [Link]));
}

// Add custom headers to ALL responses


const response = [Link]();
[Link]('X-Frame-Options', 'DENY');
[Link]('X-Content-Type-Options', 'nosniff');
return response;
}

Middleware cannot import server-only modules


Middleware runs on the Edge Runtime, which is a lightweight version of [Link]. You cannot
import Prisma, bcrypt, or most npm packages. Middleware should only do simple checks (token
presence, path matching). Heavy logic like token verification with a database lookup should
happen inside the page or API route.

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 33 of 58


MHR Internship | [Link]: From Zero to Production

CH
Styling in [Link]
09 CSS Modules, Tailwind CSS, and global styles

9.1 Four Ways to Style in [Link]


Method Scope Best for

Global CSS ([Link]) Entire app CSS resets, typography, CSS


variables
CSS Modules (.[Link]) Single component Component-level styles without
conflicts
Tailwind CSS Entire app (utility classes) Rapid UI development
CSS-in-JS (styled- Single component Dynamic styles (requires
components) special config)

9.2 Global CSS


/* src/app/[Link] */
/* Imported in [Link] — applies to every page */

/* CSS Custom Properties (Variables) */


:root {
--color-primary: #F59E0B;
--color-dark: #0F172A;
--color-text: #1E293B;
--color-bg: #FFFFFF;
--font-body: 'Inter', sans-serif;
--radius: 8px;
}

/* CSS Reset */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

body {
font-family: var(--font-body);
color: var(--color-text);
background: var(--color-bg);
line-height: 1.6;
}

h1, h2, h3 { line-height: 1.2; font-weight: 700; }


a { color: var(--color-primary); text-decoration: none; }

9.3 CSS Modules — Scoped Styles


CSS Modules automatically scope styles to the component they are imported in, preventing class
name conflicts across components.
/* src/components/[Link] */

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 34 of 58


MHR Internship | [Link]: From Zero to Production

.card {
border-radius: 12px;
padding: 24px;
background: #FFFFFF;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease;
}

.card:hover { transform: translateY(-4px); }

.title { font-size: 1.25rem; font-weight: 700; color: #0F172A; }

.badge {
display: inline-block;
padding: 4px 12px;
background: #FEF3C7;
color: #B45309;
border-radius: 100px;
font-size: 0.85rem;
}

// src/components/[Link]
import styles from './[Link]';

export default function CourseCard({ title, course, enrolled }) {


return (
<div className={[Link]}>
<h2 className={[Link]}>{title}</h2>
<span className={[Link]}>{course}</span>
{/* Combine multiple classes */}
<p className={`${[Link]} ${enrolled ? [Link] :
[Link]}`}>
{enrolled ? 'Enrolled' : 'Not enrolled'}
</p>
</div>
);
}

9.4 Tailwind CSS — Rapid Styling


Tailwind is a utility-first CSS framework. Instead of writing CSS, you apply pre-built utility classes
directly in your JSX. It is already set up if you chose it during project creation.
// src/components/[Link] — styled with Tailwind
export default function CourseCard({ title, course, enrolled }) {
return (
<div className='rounded-xl p-6 bg-white shadow-md hover:-translate-y-1
transition-transform'>
<h2 className='text-xl font-bold text-slate-900 mb-2'>{title}</h2>
<span className='inline-block px-3 py-1 bg-amber-100 text-amber-700
rounded-full text-sm'>
{course}
</span>
<p className={`mt-3 font-medium ${enrolled ? 'text-green-600' : 'text-
slate-400'}`}>
{enrolled ? 'Enrolled' : 'Not enrolled'}
</p>
</div>
);
}

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 35 of 58


MHR Internship | [Link]: From Zero to Production

Common Tailwind Classes Reference


Category Classes

Layout flex, grid, block, hidden, container, mx-auto


Spacing p-4, px-6, py-2, m-4, mx-auto, gap-4, space-y-4
Typography text-xl, font-bold, text-slate-900, leading-relaxed
Colors bg-white, bg-slate-100, text-amber-500, border-
slate-200
Sizing w-full, h-screen, max-w-3xl, min-h-[200px]
Borders rounded-xl, border, border-2, border-slate-200
Shadows shadow-sm, shadow-md, shadow-lg, shadow-xl
Hover/Focus hover:bg-amber-500, focus:outline-none,
active:scale-95
Responsive sm:text-lg, md:flex, lg:grid-cols-3

9.5 Custom Fonts with next/font


[Link] has a built-in font optimisation system that loads Google Fonts without any network
requests to Google — improving privacy and performance.
// src/app/[Link]
import { Inter, Poppins } from 'next/font/google';

const inter = Inter({


subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
});

const poppins = Poppins({


subsets: ['latin'],
weight: ['400', '600', '700'],
variable: '--font-poppins',
display: 'swap',
});

export default function RootLayout({ children }) {


return (
<html lang='en' className={`${[Link]} ${[Link]}`}>
<body className='font-inter'>{children}</body>
</html>
);
}

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 36 of 58


MHR Internship | [Link]: From Zero to Production

CH
Authentication
10 Login, sessions, and protecting your app

10.1 Understanding Authentication in [Link]


Authentication is the process of verifying who a user is. In [Link], this involves three layers
working together:

Layer Responsibility [Link] location

Authentication Verify identity (password API Route or Server Action


check, OAuth)
Session Management Keep the user logged in across Cookies (HTTP-only)
requests
Authorization Control what authenticated Middleware + Server
users can access Components

10.2 Building Authentication from Scratch (JWT)


We will build a complete, simple authentication system using JSON Web Tokens (JWT) and
HTTP-only cookies. This approach works without any third-party auth library.

npm install jsonwebtoken bcryptjs

Step 1 — The Register API


// src/app/api/auth/register/[Link]
import { NextResponse } from 'next/server';
import bcrypt from 'bcryptjs';

// In production, use a real database


let users = [];

export async function POST(request) {


const { name, email, password } = await [Link]();

// Check if user already exists


if ([Link](u => [Link] === email)) {
return [Link]({ error: 'Email already registered' }, { status:
409 });
}

// Hash the password (never store plain text!)


const hashedPassword = await [Link](password, 12);

const newUser = {
id: [Link] + 1,
name,
email,

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 37 of 58


MHR Internship | [Link]: From Zero to Production

password: hashedPassword,
role: 'student',
};
[Link](newUser);

// Do not return the password


const { password: _, ...userWithoutPassword } = newUser;
return [Link](userWithoutPassword, { status: 201 });
}

Step 2 — The Login API


// src/app/api/auth/login/[Link]
import { NextResponse } from 'next/server';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';

export async function POST(request) {


const { email, password } = await [Link]();

// Find user
const user = [Link](u => [Link] === email);
if (!user) {
return [Link]({ error: 'Invalid credentials' }, { status: 401
});
}

// Compare password
const isValid = await [Link](password, [Link]);
if (!isValid) {
return [Link]({ error: 'Invalid credentials' }, { status: 401
});
}

// Create JWT token


const token = [Link](
{ userId: [Link], email: [Link], role: [Link] },
[Link].JWT_SECRET,
{ expiresIn: '7d' }
);

// Set token in an HTTP-only cookie (more secure than localStorage)


const response = [Link]({
message: 'Login successful',
user: { id: [Link], name: [Link], email: [Link], role: [Link] }
});

[Link]({
name: 'auth-token',
value: token,
httpOnly: true, // Cannot be read by JavaScript — protects from XSS
secure: [Link].NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/',
});

return response;
}

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 38 of 58


MHR Internship | [Link]: From Zero to Production

Step 3 — The Logout API


// src/app/api/auth/logout/[Link]
import { NextResponse } from 'next/server';

export async function POST() {


const response = [Link]({ message: 'Logged out' });
[Link]('auth-token');
return response;
}

Step 4 — Reading the Session in Server Components


// src/lib/[Link] — Helper to read the current user
import { cookies } from 'next/headers';
import jwt from 'jsonwebtoken';

export async function getCurrentUser() {


const cookieStore = await cookies();
const token = [Link]('auth-token')?.value;

if (!token) return null;

try {
const decoded = [Link](token, [Link].JWT_SECRET);
return decoded; // { userId, email, role }
} catch {
return null; // Token invalid or expired
}
}

// src/app/dashboard/[Link] — Protected Server Component


import { getCurrentUser } from '@/lib/auth';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {


const user = await getCurrentUser();
if (!user) redirect('/login'); // Not logged in

return (
<div>
<h1>Welcome, {[Link]}</h1>
<p>Your role: {[Link]}</p>
</div>
);
}

Step 5 — The Login Page (Client Component)


// src/app/login/[Link]
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default function LoginPage() {


const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const router = useRouter();

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 39 of 58


MHR Internship | [Link]: From Zero to Production

async function handleSubmit(e) {


[Link]();
setLoading(true);
setError('');

const formData = new FormData([Link]);


const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: [Link]({
email: [Link]('email'),
password: [Link]('password'),
}),
});

if ([Link]) {
[Link]('/dashboard');
[Link](); // Refresh server components to pick up new cookie
} else {
const data = await [Link]();
setError([Link]);
setLoading(false);
}
}

return (
<form onSubmit={handleSubmit}>
<h1>Login to MHR Platform</h1>
<input name='email' type='email' placeholder='Email' required />
<input name='password' type='password' placeholder='Password' required />
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type='submit' disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
);
}

Role-Based Access Control (RBAC)


Use the [Link] field in JWT to restrict content. In Server Components: if ([Link] !== 'admin')
redirect('/dashboard'). In Middleware: check the decoded role and redirect accordingly. This lets
you have student, instructor, and admin roles with different permissions.

In production, use [Link] or [Link]


Building auth from scratch is great for learning, but production apps should use a battle-tested
library like [Link] (now called [Link]). It handles OAuth (Google, GitHub login), session
management, and security edge cases that are easy to get wrong. Install with: npm install next-
auth@beta

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 40 of 58


MHR Internship | [Link]: From Zero to Production

CH
Images, Metadata & SEO
11 Optimise your app for performance and search engines

11.1 The next/image Component


The built-in Image component from next/image is a massive upgrade over the plain HTML <img>
tag. It automatically handles: resizing to the correct size, converting to modern formats (WebP,
AVIF), lazy loading, preventing layout shift (CLS), and serving images through a CDN.

// src/components/[Link]
import Image from 'next/image';

export default function CourseBanner() {


return (
<div>
{/* Local image from the public/ folder */}
<Image
src='/images/[Link]'
alt='[Link] Course at MHR'
width={800}
height={400}
priority // Load immediately (for above-the-fold images)
/>

{/* Remote image (must configure the domain in [Link]) */}


<Image
src='[Link]
alt='Student coding'
width={600}
height={400}
loading='lazy' // Default — only load when in viewport
/>

{/* Fill the parent container */}


<div style={{ position: 'relative', width: '100%', height: '300px' }}>
<Image
src='/[Link]'
alt='Hero image'
fill
style={{ objectFit: 'cover' }}
/>
</div>
</div>
);
}

// [Link] — Allow remote image domains


const nextConfig = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: '[Link]' },
{ protocol: 'https', hostname: '[Link]', pathname: '/media/**'
},
],
},

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 41 of 58


MHR Internship | [Link]: From Zero to Production

};
export default nextConfig;

11.2 Metadata — Controlling Page Titles, Descriptions & OG Images


Metadata controls what appears in search engine results and social media previews. The App
Router uses a metadata export inside [Link] or [Link].

Static Metadata
// src/app/courses/[Link]
export const metadata = {
title: 'Courses | MHR Learning Platform',
description: 'Learn [Link], [Link], React Native and Cloud Computing at
MHR.',
keywords: ['React', '[Link]', 'Rwanda', 'EdTech', 'coding bootcamp'],
authors: [{ name: 'MHR Team', url: '[Link] }],

// Open Graph — controls how your page looks when shared on social media
openGraph: {
title: 'Courses | MHR Learning Platform',
description: 'World-class tech education in Rwanda.',
url: '[Link]
siteName: 'MHR Learning Platform',
images: [{ url: '[Link] width: 1200, height: 630
}],
type: 'website',
},

// Twitter Card
twitter: {
card: 'summary_large_image',
title: 'MHR Courses',
description: 'Learn to code in Rwanda.',
images: ['[Link]
},
};

export default function CoursesPage() { ... }

Dynamic Metadata — generateMetadata


For dynamic routes, use the generateMetadata function to create metadata based on the page
parameters and fetched data.
// src/app/courses/[id]/[Link]

// This runs on the server before the page renders


export async function generateMetadata({ params }) {
const course = await fetch(`/api/courses/${[Link]}`).then(r => [Link]());

// If course not found, return default metadata


if (!course) return { title: 'Course Not Found' };

return {
title: `${[Link]} | MHR`,

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 42 of 58


MHR Internship | [Link]: From Zero to Production

description: [Link],
openGraph: {
title: [Link],
description: [Link],
images: [{ url: [Link] }],
},
};
}

export default async function CourseDetailPage({ params }) {


const course = await fetch(`/api/courses/${[Link]}`).then(r => [Link]());
return <h1>{[Link]}</h1>;
}

Metadata Templates — Consistent Title Format


// src/app/[Link] — Set a title template
export const metadata = {
title: {
template: '%s | MHR Learning Platform', // %s = the page title
default: 'MHR Learning Platform', // Fallback if no page title
},
description: 'The best tech education in Rwanda.',
};

// Now in any page:


// export const metadata = { title: 'Courses' };
// → Browser tab shows: 'Courses | MHR Learning Platform'

11.3 [Link] and [Link]


// src/app/[Link] — Auto-generated sitemap
export default async function sitemap() {
const courses = await fetch('/api/courses').then(r => [Link]());

const courseUrls = [Link](course => ({


url: `[Link]
lastModified: [Link],
changeFrequency: 'monthly',
priority: 0.8,
}));

return [
{ url: '[Link] lastModified: new Date(), priority: 1.0 },
{ url: '[Link] lastModified: new Date(), priority: 0.9 },
{ url: '[Link] lastModified: new Date(), priority: 0.5 },
...courseUrls,
];
}

// src/app/[Link] — [Link]
export default function robots() {
return {
rules: [
{ userAgent: '*', allow: '/', disallow: ['/dashboard/', '/admin/'] },
],
sitemap: '[Link]

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 43 of 58


MHR Internship | [Link]: From Zero to Production

};
}

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 44 of 58


MHR Internship | [Link]: From Zero to Production

CH
Database Integration with Prisma
12 Connect [Link] to a real PostgreSQL database

12.1 What is Prisma?


Prisma is a modern database ORM (Object-Relational Mapper) for [Link] and TypeScript. It lets
you interact with your database using JavaScript objects and functions instead of writing raw SQL.
Prisma supports PostgreSQL, MySQL, SQLite, MongoDB, and more.

Without Prisma (raw SQL) With Prisma

SELECT * FROM students WHERE id = 1 [Link]({ where: { id: 1 } })


INSERT INTO students (name, email) VALUES [Link]({ data: { name: 'Alice',
('Alice', 'a@[Link]') email: 'a@[Link]' } })
UPDATE students SET name = 'Bob' WHERE [Link]({ where: { id: 1 }, data: {
id = 1 name: 'Bob' } })
DELETE FROM students WHERE id = 1 [Link]({ where: { id: 1 } })

12.2 Setting Up Prisma


# Install Prisma
npm install prisma @prisma/client

# Initialise Prisma (creates prisma/ folder and [Link])


npx prisma init --datasource-provider postgresql

# .env is automatically updated with a DATABASE_URL placeholder


# Update it with your actual connection string:
# DATABASE_URL='postgresql://user:password@localhost:5432/mhrdb'

Defining Your Schema — prisma/[Link]


// prisma/[Link]

generator client {
provider = 'prisma-client-js'
}

datasource db {
provider = 'postgresql'
url = env('DATABASE_URL')
}

model User {
id Int @id @default(autoincrement())
name String
email String @unique
password String

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 45 of 58


MHR Internship | [Link]: From Zero to Production

role Role @default(STUDENT)


createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

// Relations
enrollments Enrollment[]
}

model Course {
id Int @id @default(autoincrement())
title String
description String
weeks Int
published Boolean @default(false)
createdAt DateTime @default(now())

enrollments Enrollment[]
}

model Enrollment {
id Int @id @default(autoincrement())
userId Int
courseId Int
progress Int @default(0) // Percentage 0-100
enrolledAt DateTime @default(now())

user User @relation(fields: [userId], references: [id])


course Course @relation(fields: [courseId], references: [id])

@@unique([userId, courseId]) // A user can enroll in a course only once


}

enum Role {
STUDENT
INSTRUCTOR
ADMIN
}

# Apply schema to the database


npx prisma db push # For development (no migration files)

# OR for production-ready migrations:


npx prisma migrate dev --name init

# Open the visual database browser


npx prisma studio # Opens at [Link]

# Generate Prisma client (re-run after schema changes)


npx prisma generate

12.3 Creating the Prisma Client Singleton


In [Link], avoid creating a new Prisma client on every request. Use the singleton pattern:
// src/lib/[Link]
import { PrismaClient } from '@prisma/client';

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 46 of 58


MHR Internship | [Link]: From Zero to Production

// Prevent multiple client instances during development hot reload


const globalForPrisma = global;

export const prisma =


[Link] ?? new PrismaClient({ log: ['query'] });

if ([Link].NODE_ENV !== 'production') {


[Link] = prisma;
}

12.4 CRUD Operations with Prisma in API Routes


// src/app/api/students/[Link]
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';

// GET all students with their enrollments


export async function GET() {
const students = await [Link]({
where: { role: 'STUDENT' },
select: {
id: true,
name: true,
email: true,
createdAt: true,
_count: { select: { enrollments: true } }, // Count enrollments
},
orderBy: { createdAt: 'desc' },
});

return [Link](students);
}

// POST — create a student


export async function POST(request) {
const { name, email, password } = await [Link]();

try {
const student = await [Link]({
data: { name, email, password, role: 'STUDENT' },
select: { id: true, name: true, email: true }, // Never return password
});
return [Link](student, { status: 201 });
} catch (error) {
if ([Link] === 'P2002') { // Prisma unique constraint violation
return [Link]({ error: 'Email already exists' }, { status: 409
});
}
return [Link]({ error: 'Server error' }, { status: 500 });
}
}

Complex Queries — Filtering, Sorting, Pagination


// GET /api/courses?search=react&published=true&page=1&limit=10
export async function GET(request) {
const { searchParams } = new URL([Link]);

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 47 of 58


MHR Internship | [Link]: From Zero to Production

const search = [Link]('search') ?? '';


const published = [Link]('published');
const page = Number([Link]('page') ?? '1');
const limit = Number([Link]('limit') ?? '10');
const skip = (page - 1) * limit;

const [courses, total] = await [Link]([


[Link]({
where: {
published: published !== null ? published === 'true' : undefined,
OR: [
{ title: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } },
],
},
include: { _count: { select: { enrollments: true } } },
orderBy: { createdAt: 'desc' },
take: limit,
skip,
}),
[Link]({ where: { published: published !== null ? published
=== 'true' : undefined } }),
]);

return [Link]({
courses,
pagination: { page, limit, total, pages: [Link](total / limit) },
});
}

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 48 of 58


MHR Internship | [Link]: From Zero to Production

CH
Performance & Optimisation
13 Making your [Link] app fast

13.1 Understanding [Link] Performance


[Link] is built for performance by default. But knowing WHY certain things are fast helps you
make better architectural decisions. Here are the key performance concepts:

Concept What it means How [Link] handles it

Core Web Vitals Google's metrics for page Optimized rendering, image
experience component, font loading
LCP (Largest Contentful Paint) How fast the main content SSG, ISR, priority images
loads
FID/INP (Interaction to Next How fast the page responds to Minimal JS bundle (Server
Paint) clicks Components)
CLS (Cumulative Layout Shift) Does layout jump around while Image width/height prevents
loading? layout shift
TTFB (Time to First Byte) How fast the server responds Edge caching, CDN
deployment

13.2 Streaming and Suspense


Streaming lets you progressively send parts of a page to the browser as they become ready,
instead of waiting for ALL data to load before sending anything. Combine this with React
Suspense for a great user experience.
// src/app/dashboard/[Link]
import { Suspense } from 'react';
import StudentStats from '@/components/StudentStats';
import RecentActivity from '@/components/RecentActivity';
import CourseProgress from '@/components/CourseProgress';

export default function DashboardPage() {


return (
<div>
<h1>Dashboard</h1>

{/* Each Suspense boundary streams independently */}


{/* Fast data loads first, slow data loads progressively */}

<Suspense fallback={<p>Loading stats...</p>}>


<StudentStats /> {/* Fetches student count, completion rate */}
</Suspense>

<Suspense fallback={<p>Loading activity...</p>}>


<RecentActivity /> {/* Fetches recent logins, submissions */}
</Suspense>

<Suspense fallback={<p>Loading progress...</p>}>

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 49 of 58


MHR Internship | [Link]: From Zero to Production

<CourseProgress /> {/* Fetches course completion data */}


</Suspense>
</div>
);
}

// Each child component fetches its own data independently


// src/components/[Link] (Server Component)
export default async function StudentStats() {
const stats = await [Link]({ where: { role: 'STUDENT' } });
return <div><h2>Total Students: {stats}</h2></div>;
}

13.3 Bundle Size Optimisation

Dynamic Imports — Code Splitting


Import heavy components only when they are needed using dynamic imports. This reduces the
initial JavaScript bundle size.
import dynamic from 'next/dynamic';

// This component is only loaded when it appears on screen


const HeavyChartComponent = dynamic(
() => import('@/components/HeavyChartComponent'),
{
loading: () => <p>Loading chart...</p>,
ssr: false, // Do not render on server (for browser-only libraries)
}
);

// Modal — only load the JS when the modal is opened


const VideoPlayer = dynamic(() => import('@/components/VideoPlayer'), {
ssr: false,
});

export default function CoursePage() {


const [showVideo, setShowVideo] = useState(false);
return (
<div>
<button onClick={() => setShowVideo(true)}>Watch Intro Video</button>
{showVideo && <VideoPlayer src='/intro.mp4' />}
</div>
);
}

Analysing Your Bundle


npm install @next/bundle-analyzer

# [Link]
import bundleAnalyzer from '@next/bundle-analyzer';
const withBundleAnalyzer = bundleAnalyzer({ enabled: [Link] ===
'true' });
export default withBundleAnalyzer(nextConfig);

# Run:

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 50 of 58


MHR Internship | [Link]: From Zero to Production

ANALYZE=true npm run build


# Opens a visual map of your JavaScript bundle in the browser

Performance Checklist
Use Server Components by default. Use dynamic() for large client-side libraries. Add width/height
to all <Image> tags. Set priority on above-the-fold images. Use next/font for custom fonts. Run
npm run build and check the output for large page sizes.

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 51 of 58


MHR Internship | [Link]: From Zero to Production

CH
Deployment
14 Taking your app live with Vercel and other platforms

14.1 Preparing for Production


Before deploying, always run the production build locally to catch any errors:
npm run build

# The output shows:


# ○ Static — pre-rendered as static HTML
# ƒ Dynamic — server-rendered on demand
# ● ISR — incrementally static regenerated

# Common build errors to fix:


# - Missing environment variables
# - Images without alt text (accessibility warning)
# - Unhandled promise rejections
# - Missing required props

# Test the production build locally:


npm run start # Visit [Link]

14.2 Deploying to Vercel — The Easiest Option


Vercel is the company that created [Link]. Their platform provides the best [Link] support with
zero configuration — all features including SSR, ISR, Edge Functions, and image optimisation
work out of the box.

Step-by-Step Vercel Deployment


1. Push your project to a GitHub repository
2. Go to [Link] and sign up with your GitHub account
3. Click 'Add New Project' and import your repository
4. Vercel auto-detects [Link] and configures build settings
5. Add environment variables: click 'Environment Variables' and add all values from .[Link]
6. Click 'Deploy' — your app is live in about 60 seconds
7. Every future git push to main triggers an automatic re-deployment

# Setting up Vercel CLI (optional — for deployments from terminal)


npm install -g vercel
vercel login
vercel # Deploy preview
vercel --prod # Deploy to production

Preview Deployments
Every pull request and every non-main branch gets its own preview URL automatically. This lets
you share a working demo of a feature before merging it.

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 52 of 58


MHR Internship | [Link]: From Zero to Production

git checkout -b feature/new-course-page


# ... make changes ...
git push origin feature/new-course-page
# Vercel creates: [Link]
# Share this URL with your team for review!

14.3 Custom Domains


# In Vercel Dashboard: Settings > Domains
# Add your domain: [Link]

# Then in your domain registrar (e.g., GoDaddy, Namecheap):


# Add a CNAME record pointing to: [Link]
# OR add an A record pointing to: [Link]

# Vercel automatically provisions an SSL certificate


# Your site will be live at [Link] within minutes

14.4 Environment Variables on Vercel


# In Vercel Dashboard: Project Settings > Environment Variables

# Add each variable from your .[Link]:


DATABASE_URL = postgresql://... (your production database URL)
JWT_SECRET = (a strong random string — use: openssl rand -base64 32)
NEXT_PUBLIC_APP_URL = [Link]

# Scope environment variables per environment:


# Production — runs when deployed to main branch
# Preview — runs for all preview deployments
# Development — runs locally with 'vercel env pull'

14.5 Deploying to Other Platforms


[Link] can also be deployed to other platforms. Here is how:

Platform Command / Config Notes

Vercel Auto-detected Best for [Link] — zero config


Railway nixpacks auto-detects [Link] Great for [Link] +
PostgreSQL together
Andasy npm run build, start command: Great for African market
npm run start
Docker Custom Dockerfile needed Full control, good for enterprise
AWS / GCP Use adapters or Docker Complex setup but maximum
flexibility

Dockerfile for [Link]

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 53 of 58


MHR Internship | [Link]: From Zero to Production

# Dockerfile
FROM node:20-alpine AS base

FROM base AS deps


WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM base AS builder


WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM base AS runner


WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

EXPOSE 3000
CMD ['node', '[Link]']

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 54 of 58


MHR Internship | [Link]: From Zero to Production

CH
Capstone Project
15 Build a complete MHR Learning Platform

15.1 Project Overview


This capstone project puts together everything you have learned across all 14 chapters. You will
build a fully functional, production-deployed Learning Management System (LMS) for Mastery Hub
of Rwanda.

What you will demonstrate


File-based routing, Layouts, Server + Client Components, Data fetching strategies, API Routes,
Server Actions, Prisma + PostgreSQL, Authentication with JWT, Middleware route protection,
Metadata and SEO, Tailwind CSS styling, Image optimisation, and deployment to Vercel or
Andasy.

15.2 Features to Build

Public Pages (no login required)


• Home page — hero section, featured courses, testimonials
• Courses page — searchable/filterable list with SSR
• Course detail page — full description, instructor, enrolment CTA
• About page — MHR mission and team
• Login and Register pages

Student Dashboard (login required)


• Dashboard home — enrolled courses, progress overview
• My Courses — list of enrolled courses with progress bars
• Course Player — lesson content, mark as complete
• Profile page — update name, avatar, password

Admin Panel (admin role required)


• Manage Courses — create, edit, publish/unpublish
• Manage Students — view, search, enroll/remove
• Analytics — enrollment counts, completion rates

15.3 Recommended Project Structure


src/
├── app/
│ ├── [Link] # Root layout (NavBar, Footer)
│ ├── [Link] # Home page
│ ├── courses/
│ │ ├── [Link] # Courses list (SSR)

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 55 of 58


MHR Internship | [Link]: From Zero to Production

│ │ └── [id]/
│ │ └── [Link] # Course detail (ISR)
│ ├── about/[Link]
│ ├── login/[Link] # Client Component
│ ├── register/[Link] # Client Component
│ ├── dashboard/
│ │ ├── [Link] # Dashboard sidebar layout
│ │ ├── [Link] # Dashboard home
│ │ ├── courses/[Link] # My enrolled courses
│ │ └── profile/[Link] # Profile settings
│ ├── admin/
│ │ ├── [Link] # Admin sidebar
│ │ ├── [Link] # Admin dashboard
│ │ └── courses/
│ │ ├── [Link]
│ │ └── [id]/[Link]
│ └── api/
│ ├── auth/
│ │ ├── login/[Link]
│ │ ├── register/[Link]
│ │ └── logout/[Link]
│ ├── courses/
│ │ ├── [Link]
│ │ └── [id]/[Link]
│ ├── enrollments/[Link]
│ └── users/
│ └── [id]/[Link]
├── actions/
│ ├── [Link] # Server Actions: register, login
│ ├── [Link] # Server Actions: create, update
│ └── [Link] # Server Actions: enroll, unenroll
├── components/
│ ├── [Link]
│ ├── [Link]
│ ├── [Link] # 'use client'
│ ├── [Link]
│ └── ui/ # Reusable UI components
│ ├── [Link]
│ ├── [Link]
│ └── [Link]
├── lib/
│ ├── [Link] # Prisma singleton
│ └── [Link] # getCurrentUser helper
├── actions/
│ └── ... server actions ...
└── [Link] # Route protection

15.4 Database Schema for the Capstone


// prisma/[Link]

model User {
id Int @id @default(autoincrement())
name String
email String @unique
password String
role Role @default(STUDENT)
avatar String?
createdAt DateTime @default(now())
enrollments Enrollment[]
}

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 56 of 58


MHR Internship | [Link]: From Zero to Production

model Course {
id Int @id @default(autoincrement())
title String
description String
thumbnail String?
duration Int // in days
published Boolean @default(false)
createdAt DateTime @default(now())
lessons Lesson[]
enrollments Enrollment[]
}

model Lesson {
id Int @id @default(autoincrement())
courseId Int
title String
content String // Markdown or HTML
order Int // Lesson order within the course
course Course @relation(fields: [courseId], references: [id])
completions LessonCompletion[]
}

model Enrollment {
id Int @id @default(autoincrement())
userId Int
courseId Int
progress Int @default(0)
enrolledAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
course Course @relation(fields: [courseId], references: [id])
@@unique([userId, courseId])
}

model LessonCompletion {
id Int @id @default(autoincrement())
userId Int
lessonId Int
completedAt DateTime @default(now())
lesson Lesson @relation(fields: [lessonId], references: [id])
@@unique([userId, lessonId])
}

enum Role { STUDENT INSTRUCTOR ADMIN }

15.5 Assessment Rubric


Criterion Weight Full marks requires

Routing & Layouts 15% All pages exist, nested layouts


work, 404 page present
Server vs Client Components 15% Correct usage throughout, no
unnecessary 'use client'
Data Fetching 15% Appropriate strategy per page
(SSR/SSG/ISR), error handling
API Routes / Server Actions 15% Full CRUD, input validation,
proper HTTP status codes

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 57 of 58


MHR Internship | [Link]: From Zero to Production

Criterion Weight Full marks requires

Authentication & Middleware 20% Register, login, logout,


protected routes, role-based
access
Styling & UI Quality 10% Responsive, consistent design
with Tailwind
Deployment 10% Live on Vercel/Andasy, env
vars configured, custom
domain (bonus)

Presentation Day Tips


Demo the full user journey: register → browse courses → enroll → view lesson → mark complete
→ see progress on dashboard. Then show the admin panel creating a new course. Finally, show
your Vercel dashboard with the deployment logs. Be ready to explain one architectural decision
you made and why.

15.6 Next Steps After This Course


You have covered the core of [Link] development. Here is what to explore next to go from
intermediate to advanced:

Libraries Worth Learning


• [Link] / [Link] — production-grade authentication with OAuth (Google, GitHub)
• TanStack Query (React Query) — powerful data fetching and caching for Client
Components
• Zustand or Jotai — lightweight global state management
• Shadcn/ui — beautiful, accessible component library built on Tailwind
• Zod — schema validation for forms and API inputs
• React Hook Form — performant form management

Advanced [Link] Topics


• Parallel Routes and Intercepting Routes — for complex UIs like modals
• Edge Runtime — running Middleware and API routes closer to users
• Partial Pre-rendering — mixing static and dynamic content on a single page
• Turbopack — the next-generation bundler replacing Webpack
• Testing — Jest + React Testing Library + Playwright for E2E tests

Keep Building
The best way to solidify [Link] knowledge is to build. Start with small projects: a personal
portfolio, a blog with a CMS, a simple e-commerce site. Each project will reveal new challenges
and force you to go deeper into the documentation. The official [Link] docs at [Link]/docs are
excellent — bookmark them.

© 2025 Mastery Hub of Rwanda | Kigali, Rwanda | Page 58 of 58

You might also like