22. Real-World Projects
Beginner Projects
Static Portfolio Site
The first project is a static showcase of personal work built with Next.js, Tailwind CSS, and Markdown for blog‑style entries. The site is deployed on Vercel, taking advantage of its global edge network for instant loading.
- Styling: Tailwind’s utility‑first classes enable rapid UI construction without leaving the HTML.
- Dark mode: Implemented via
next-themes; a simple toggle switches thedata-themeattribute on the<html>element. - Contact form: Uses Formspree; the form posts to a Formspree endpoint, which forwards submissions to an email address.
- SEO: Metadata is injected with
next/head, allowing each page to define its own<title>,<meta name="description">, and Open Graph tags.
Example of a page’s head section:
import Head from 'next/head';
export default function Home() {
return (
<>
<Head>
<title>My Portfolio – Projects & Blog</title>
<meta name="description" content="Showcase of my Next.js projects, blog posts, and open‑source contributions." />
<meta property="og:title" content="My Portfolio" />
<meta property="og:image" content="/og-image.jpg" />
</Head>
{/* page content */}
</>
);
}
MDX‑Enabled Blog Platform
Building on the portfolio, the blog adds MDX support, author bios, pagination, and an RSS feed. Performance optimizations include automatic font loading with next/font, Incremental Static Regeneration (ISR), and image optimization via next/image.
- MDX: Allows mixing JSX components inside Markdown, enabling interactive demos within posts.
- Author bios: Stored as JSON files; each post imports the relevant author object.
- Pagination: Implemented with
getStaticPropsreturning a limited set of posts plusnextPage/prevPageURLs. - RSS feed: Generated at build time using the
rsspackage; the feed URL is/rss.xml. - Font optimization:
next/fontautomatically self‑hosts Google Fonts, eliminating external requests. - ISR:
export const revalidate = 60;tells Next.js to regenerate the page at most once every 60 seconds. - Image optimization:
next/imageserves responsively sized images with lazy loading and placeholder blur.
Sample ISR page:
export const revalidate = 60; // revalidate every 60 seconds
export async function getStaticProps() {
const posts = await fetchPosts(); // fetch from CMS or filesystem
return { props: { posts } };
}
export default function Blog({ posts }) {
return (
<ul>
{posts.map(p => (
<li key={p.id}>
<Link href={`/blog/${p.slug}`}>{p.title}</Link>
</li>
))}
</ul>
);
}
CRUD Notes Application
The notes app demonstrates full‑cycle state management with React Hook Form, schema validation via Zod, and a data‑migration strategy that moves notes from localStorage to a PlanetScale‑hosted SQLite‑compatible database using Prisma.
- State management: React Hook Form handles form values, errors, and resets with minimal boilerplate.
- Zod schema: Enforces a minimum title length and ensures content exists.
- Persistence layer: Initial version writes to
localStoragefor instant feedback; a background sync writes to Prisma, which then pushes to PlanetScale. - Prisma schema: Defines a
Notemodel withtitleandcontentfields.
Zod validation example:
import { z } from 'zod';
const noteSchema = z.object({
title: z.string().min(3, 'Title must be at least 3 characters'),
content: z.string().nonempty('Content is required'),
});
export default function NoteForm() {
const { register, handleSubmit, reset } = useForm({ resolver: zodResolver(noteSchema) });
const onSubmit = data => {
// data is guaranteed to match the schema
createNote(data);
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('title')} placeholder="Title" />
<textarea {...register('content')} placeholder="Note" />
<button type="submit">Save</button>
</form>
);
}
Prisma model:
model Note {
id Int @id @default(autoincrement())
title String
content String?
createdAt DateTime @default(now())
}
Intermediate Projects
E‑commerce Store
This project implements a full product catalog, shopping cart, Stripe checkout, and an admin dashboard for order management. Server Actions handle mutations, Redis caches cart data, and next/image optimizes product thumbnails.
- Product catalog: Fetched from a PostgreSQL database via Prisma; each product includes images, price, and inventory.
- Cart: Stored in Redis as a hash
cart:{userId}where fields are product IDs and values are quantities. - Stripe integration: Uses Stripe Checkout for one‑time payments; webhook endpoint verifies
checkout.session.completedevents. - Admin dashboard: Protected route that lists orders, allows status updates, and exports CSV reports.
- Server Actions: Enable async functions to be called directly from form elements, reducing API boilerplate.
Server Action for adding to cart:
'use server';
import { redis } from '@/lib/redis';
export async function addToCart(formData: FormData) {
const userId = formData.get('userId') as string;
const productId = formData.get('productId') as string;
const qty = parseInt(formData.get('qty') as string, 10) || 1;
await redis.hset(`cart:${userId}`, productId, qty.toString());
return { success: true };
}
Calling the action from a form:
<form action={addToCart}>
<input type="hidden" name="userId" value="{userId}" />
<input type="hidden" name="productId" value="{productId}" />
<input type="number" name="qty" defaultValue="1" />
<button type="submit">Add to cart</button>
Learning Management System (LMS)
The LMS supports course creation, video streaming through Mux, interactive quizzes powered by React‑Query, role‑based access control via NextAuth.js (JWT strategy), and automatic certificate generation with pdfkit.
- Course creation: Instructors upload video files; Mux creates streaming URLs and generates thumbnails.
- Video streaming: Uses Mux Player (
@mux/mux-player) for adaptive HLS/DASH playback. - Quizzes: Data fetched with React‑Query; optimistic updates improve perceived performance.
- Authentication: NextAuth.js with JWT stores a signed token in an HTTP‑only cookie; middleware checks role claims.
- Certificate generation: After course completion, a PDF is generated server‑side using
pdfkitand streamed to the user.
Mux video upload example:
import { Video } from '@mux/mux-node';
const { Video } = new Mux(tokenId, tokenSecret);
export async function POST(req) {
const form = await req.formData();
const file = form.get('video') as File;
const buffer = Buffer.from(await file.arrayBuffer());
const upload = await Video.Uploads.create({
asset_ids: [], // will be filled after upload completes
cors_origin: '*',
new_asset_settings: {
playback_policy: ['public'],
},
});
// PUT request to upload URL (omitted for brevity)
return new Response(JSON.stringify({ uploadId: upload.id }), { status: 200 });
}
PDF certificate generation with pdfkit:
import { PDFDocument } from 'pdfkit';
import { Readable } from 'stream';
export async function generateCertificate({ name, course }) {
const doc = new PDFDocument({ size: 'A4', margin: 50 });
const chunks: Uint8Array[] = [];
doc.on('data', chunk => chunks.push(chunk));
doc.on('end', () => {
const pdfBuffer = Buffer.concat(chunks);
// stream pdfBuffer as response
});
doc.fontSize(26).text('Certificate of Completion', { align: 'center' });
doc.moveDown();
doc.fontSize(16).text(`This certifies that ${name}`, { align: 'center' });
doc.fontSize(16).text(`has successfully completed the course "${course}".`, { align: 'center' });
doc.moveDown();
doc.fontSize(12).text(`Date: ${new Date().toLocaleDateString()}`, { align: 'right' });
doc.end();
return Readable.from(chunks);
}
Job Portal
Job listings feature search/filter via useSearchParams, resume uploads through UploadThing, application tracking, and email notifications powered by SendGrid.
- Search/filter: The URL’s query string drives the UI; updating
useSearchParamsupdates the list without a full reload. - Resume upload: UploadThing provides a secure, typed upload endpoint; files are stored in an S3‑compatible bucket.
- Application tracking: Each applicant’s status (applied, reviewed, interviewed, hired) is stored in a PostgreSQL table.
- Email notifications: SendGrid’s
@sendgrid/mailsends templated emails on status changes.
Using useSearchParams for filtering:
import { useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
export default function JobList() {
const searchParams = useSearchParams();
const [filters, setFilters] = useState({
q: searchParams.get('q') ?? '',
location: searchParams.get('location') ?? '',
remote: searchParams.get('remote')