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