Skip to main content

3.2 Modern Development Frameworks

Why Do You Need a Framework?

Imagine you're cooking a meal.

Option A: From scratch. You go to the market, wash the vegetables, chop them, mix the seasonings, manage the heat… every step is on you. One meal could take two hours, and whether it tastes good depends entirely on experience.

Option B: Use a meal kit. Ingredients are pre-washed and pre-cut, seasonings pre-measured — you just toss it in the pan for a few minutes. Fast, consistent results. A bit less flexibility, but more than good enough for most people.

A framework is the "meal kit" of programming.

Technically, you could build a website with pure HTML, CSS, and JavaScript from scratch. But you'll find that by the third page, things start falling apart. Every page repeats a pile of navbar code, changing a color requires edits in ten files, and navigating between pages causes a full-screen white flash…

Frameworks package all that "repetitive, universal, tedious" work for you. Routing, state management, styling, bundling, performance optimization — all ready out of the box.

Why are frameworks even more critical in the AI coding era? Because AI understands popular frameworks best and generates the highest quality code for them. Ask AI to write a Next.js page and it'll likely be fast and accurate. Ask it to write code for an obscure framework and it might confidently hallucinate. Choosing a mainstream framework = letting AI perform at its best.


Next.js: The Swiss Army Knife of Full-Stack Development

What Is Next.js?

Next.js is currently the most recommended web development framework, bar none. It's built on React but comes with a massive set of "batteries-included" capabilities.

Why choose it? Four reasons:

  1. Full-stack framework — Frontend pages and backend APIs live in the same project. No need to maintain two separate codebases. Think of it as Next.js giving you a "shopfront with a kitchen included" instead of renting them separately.
  2. Vercel ecosystem — The company behind Next.js, Vercel, has the most user-friendly deployment platform. Push code to GitHub, and it deploys automatically. Free tier is more than enough for personal projects.
  3. AI-friendly — Current AI coding tools (Cursor, Claude Code, etc.) have the best support for Next.js. The generated code just works.
  4. Excellent performance — Automatic code splitting, image optimization, server-side rendering — no manual optimization needed.

Creating a Project

Open your terminal and run one command:

npx create-next-app@latest my-app

It'll ask you a few questions:

✔ Would you like to use TypeScript? → Yes (type safety)
✔ Would you like to use ESLint? → Yes (catches errors early)
✔ Would you like to use Tailwind CSS? → Yes (must pick this — more below)
✔ Would you like your code inside a `src/` directory? → Yes (cleaner structure)
✔ Would you like to use App Router? → Yes (new routing system, recommended)
✔ Would you like to use Turbopack for next dev? → Yes (faster dev server)
✔ Would you like to customize the import alias? → No (default @/ is fine)

Why these choices? TypeScript helps AI understand your code better. Tailwind CSS is the best styling solution available. App Router is Next.js 13+'s recommended architecture with better performance.

Project Structure Explained

After creation, you'll see this directory structure:

my-app/
├── public/ # Static assets (images, fonts, etc.)
│ ├── next.svg # Next.js logo
│ └── vercel.svg # Vercel logo
├── src/
│ └── app/ # Core directory! All pages live here
│ ├── favicon.ico # Site icon
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout (shared shell for all pages)
│ └── page.tsx # Homepage (displayed at /)
├── next.config.ts # Next.js config file
├── package.json # Project dependencies and scripts
├── tsconfig.json # TypeScript config
├── tailwind.config.ts # Tailwind CSS config
└── postcss.config.mjs # PostCSS config (Tailwind's underlying dependency)

Focus on understanding src/app/ — this is where you'll spend most of your time. Each folder represents a route (URL), and each filename has a special meaning:

FilenamePurposeRequired?
page.tsxThe page content displayed at this route
layout.tsxLayout wrapper for child pages (navbar, sidebar, etc.)Optional
loading.tsxLoading state shown while the page loadsOptional
error.tsxError page shown when something goes wrongOptional
not-found.tsx404 pageOptional

Pages and Routing (Files Are Routes)

One of Next.js's coolest design choices: the file path IS the URL path. No manual route configuration needed — just create files.

src/app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── page.tsx → /blog (article list)
│ └── [slug]/
│ └── page.tsx → /blog/my-first-post (dynamic route)
├── dashboard/
│ ├── layout.tsx → Shared dashboard layout
│ ├── page.tsx → /dashboard
│ └── settings/
│ └── page.tsx → /dashboard/settings
└── api/
└── hello/
└── route.ts → GET /api/hello

Square brackets like [slug] create dynamic routes — meaning that position can be any value. Both /blog/hello-world and /blog/nextjs-tutorial would match blog/[slug]/page.tsx.

Here's an actual page component:

// src/app/about/page.tsx
// A plain React component — Next.js automatically turns it into a page

export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>This is a website built with Next.js.</p>
</div>
)
}
// src/app/layout.tsx — Root layout, wraps every page
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
title: "My Website",
description: "Built with Next.js + Tailwind",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="bg-gray-50 text-gray-900 antialiased">
<nav className="p-4 bg-white shadow">
<a href="/" className="mr-4">Home</a>
<a href="/about" className="mr-4">About</a>
<a href="/blog">Blog</a>
</nav>
<main className="max-w-4xl mx-auto p-6">
{children}
</main>
</body>
</html>
);
}

children is whatever the current page's page.tsx renders. Every page automatically gets wrapped with this layout (navbar + main container).

Server Components vs Client Components

Next.js App Router introduces an important concept: by default, all components are Server Components.

Server Components:

  • Render on the server — HTML is sent directly to the browser
  • Can directly access databases, read files, call APIs
  • Cannot use useState, useEffect, or event listeners (onClick, etc.)
  • Best for: displaying data, fetching content, non-interactive pages

Client Components:

  • Render in the browser and can be interactive
  • Need a "use client" declaration at the top of the file
  • Can use useState, useEffect, and event listeners
  • Best for: forms, button clicks, real-time updates, animations

How to choose? Simple rule: default to Server Components; only add "use client" when you need interactivity.

// ✅ Server Component — displays article content, no interaction needed
// src/app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: { params: { slug: string } }) {
const res = await fetch(`https://api.example.com/posts/${params.slug}`);
const post = await res.json();

return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
// ✅ Client Component — has button click interaction
// src/components/LikeButton.tsx
"use client";

import { useState } from "react";

export default function LikeButton() {
const [likes, setLikes] = useState(0);

return (
<button
onClick={() => setLikes(likes + 1)}
className="bg-pink-500 text-white px-4 py-2 rounded-lg hover:bg-pink-600"
>
❤️ Like ({likes})
</button>
);
}

Then import the Client Component directly in a Server Component:

// src/app/blog/[slug]/page.tsx
import LikeButton from "@/components/LikeButton";

export default async function BlogPost({ params }: { params: { slug: string } }) {
// ... fetch data
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton /> {/* Interactive part extracted as a separate Client Component */}
</article>
);
}

API Routes (Backend Endpoints)

Next.js API routes let you write backend logic in the same project — no separate backend server needed.

// src/app/api/hello/route.ts
import { NextResponse } from "next/server";

// GET request handler
export async function GET() {
return NextResponse.json({ message: "Hello, World!" });
}

// POST request handler
export async function POST(request: Request) {
const body = await request.json();
return NextResponse.json({
message: `Received your data: ${body.name}`,
});
}

Visit http://localhost:3000/api/hello and you'll get { "message": "Hello, World!" }.

You can call it directly from the frontend:

const res = await fetch("/api/hello");
const data = await res.json();
// data.message === "Hello, World!"

Data Fetching (Direct fetch in Server Components)

In Server Components, you can await fetch() directly — very intuitive:

// src/app/blog/page.tsx
interface Post {
id: number;
title: string;
excerpt: string;
}

export default async function BlogPage() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10");
const posts: Post[] = await res.json();

return (
<div>
<h1 className="text-3xl font-bold mb-6">Blog Posts</h1>
<div className="space-y-4">
{posts.map((post) => (
<a
key={post.id}
href={`/blog/${post.id}`}
className="block p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
>
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-gray-500 mt-1">{post.excerpt}</p>
</a>
))}
</div>
</div>
);
}

No useEffect, no useState, no loading state — the data is fetched on the server, and the browser receives fully rendered HTML.

Hands-On: A Complete Blog System

Let's tie everything together into a working blog.

// src/app/blog/[slug]/page.tsx — Dynamic blog post page

interface Post {
id: number;
title: string;
body: string;
}

export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${params.slug}`
);
const post: Post = await res.json();

return (
<article className="max-w-2xl mx-auto py-8">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="prose prose-lg">
<p>{post.body}</p>
</div>
<div className="mt-8 pt-4 border-t">
<a href="/blog" className="text-blue-500 hover:underline">
← Back to all posts
</a>
</div>
</article>
);
}
// src/app/blog/[slug]/not-found.tsx — Shown when article doesn't exist

export default function NotFound() {
return (
<div className="text-center py-20">
<h1 className="text-6xl font-bold text-gray-300">404</h1>
<p className="text-xl text-gray-500 mt-4">This post doesn't exist</p>
<a href="/blog" className="text-blue-500 hover:underline mt-4 block">
Back to blog
</a>
</div>
);
}

Visit /blog/1 to see the article. Visit /blog/99999 and you'll see the 404 page. The entire process sends zero JavaScript to the browser (Server Component), making it SEO-friendly and blazing fast.


Tailwind CSS: A New Way to Write Styles

What Is Tailwind?

Traditional CSS works like this: you give a button a class name class="submit-btn", then write in your CSS file:

.submit-btn {
background: #3b82f6;
color: white;
padding: 8px 20px;
border-radius: 8px;
font-weight: bold;
}

Over a full project, the CSS file grows to hundreds of lines, class names start overriding each other, and changing one thing breaks something elsewhere.

Tailwind takes a different approach — apply "utility classes" directly in your HTML:

<button class="bg-blue-500 text-white px-5 py-2 rounded-lg font-bold">
Submit
</button>

See that? bg-blue-500 is the blue background, text-white is white text, px-5 py-2 is padding, rounded-lg is border radius. Each class does one small thing; combined, they create a complete style.

Why Does Everyone Love Tailwind?

  1. Fast — No more jumping between HTML and CSS files. Styles are right there in the markup. AI is also great at writing Tailwind because it uses standardized class names — no need to understand your custom CSS naming conventions.
  2. Consistent — Colors, spacing, and font sizes follow fixed scales. No more "this button has 12px border radius, that one has 15px" chaos.
  3. Small bundles — Automatically removes unused CSS. Final builds are typically just a few KB.
  4. Easy responsive design — Just add a prefix for different screen sizes.

Common Classes Quick Reference

Typography:

text-xs / text-sm / text-base / text-lg / text-xl / text-2xl / text-3xl → Font size
font-normal / font-medium / font-semibold / font-bold → Font weight
text-gray-500 / text-blue-600 / text-red-500 → Color
text-center / text-left / text-right → Alignment

Background & Color:

bg-white / bg-gray-100 / bg-blue-500 / bg-green-600 → Background color
bg-gradient-to-r from-blue-500 to-purple-500 → Gradient background
opacity-50 / opacity-75 → Opacity

Flexbox Layout:

flex → Enable flex
flex-row → Horizontal layout (default)
flex-col → Vertical layout
justify-center → Center horizontally
justify-between → Space between
items-center → Center vertically
gap-4 → Gap between children

Grid Layout:

grid → Enable grid
grid-cols-3 → 3 equal columns
grid-cols-[200px_1fr] → Custom column widths
col-span-2 → Span 2 columns

Spacing (most commonly used):

p-4 / px-4 / py-4 / pt-4 / pr-4 / pb-4 / pl-4 → Padding
m-4 / mx-auto / mt-4 / mb-4 → Margin
gap-2 / gap-4 / gap-8 → Gap between children
Number mapping: 1=4px, 2=8px, 3=12px, 4=16px, 6=24px, 8=32px

Borders & Rounded Corners:

border / border-2 / border-gray-200 → Border
rounded / rounded-lg / rounded-full → Border radius (small to large)
shadow / shadow-lg / shadow-xl → Shadow

Responsive (add prefix):

sm: → ≥640px (phone landscape)
md: → ≥768px (tablet)
lg: → ≥1024px (laptop)
xl: → ≥1280px (desktop)

Usage: grid-cols-1 md:grid-cols-2 lg:grid-cols-3
Meaning: 1 column on phone, 2 on tablet, 3 on desktop

Hands-On: Building a Card Component with Tailwind

function ProductCard() {
return (
<div className="bg-white rounded-2xl shadow-lg overflow-hidden max-w-sm">
{/* Image */}
<img
src="/product.jpg"
alt="Product image"
className="w-full h-48 object-cover"
/>

{/* Content area */}
<div className="p-6">
<span className="text-xs font-medium text-blue-600 bg-blue-50 px-2 py-1 rounded-full">
New Arrival
</span>
<h3 className="text-xl font-bold text-gray-900 mt-3">
Amazing Coding Keyboard
</h3>
<p className="text-gray-500 text-sm mt-2">
Cherry MX Red switches, silky-smooth typing feel, double your coding productivity.
</p>

{/* Price and button */}
<div className="flex items-center justify-between mt-4">
<span className="text-2xl font-bold text-red-500">¥299</span>
<button className="bg-blue-500 text-white px-5 py-2 rounded-lg hover:bg-blue-600 transition-colors">
Add to Cart
</button>
</div>
</div>
</div>
);
}

Not a single line of CSS written, yet the styling is complete, responsive, and even has hover effects and transition animations. That's the Tailwind efficiency.

Dark Mode

Tailwind has built-in dark mode support — just add the dark: prefix:

function DarkModeCard() {
return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow">
<h2 className="text-gray-900 dark:text-white text-xl font-bold">
Dark Mode Supported Card
</h2>
<p className="text-gray-600 dark:text-gray-300 mt-2">
Automatically adapts to system theme, or can be toggled manually.
</p>
</div>
);
}

dark:bg-gray-800 means: when the system is in dark mode, use a dark gray background. You need to set the strategy in tailwind.config.ts:

// tailwind.config.ts
export default {
darkMode: "class", // or "media" (follows system)
// ...
};

Next.js + Tailwind: The Dream Team

Together, these two form the most mainstream combination today. Next.js's scaffolding has Tailwind built in when you create a project — ready to use out of the box.

The division of labor is crystal clear: Next.js handles "page structure and data logic," Tailwind handles "visual styling."

Here's a typical example of them working together:

// src/app/page.tsx — Homepage
export default async function HomePage() {
const res = await fetch("https://api.example.com/stats");
const stats = await res.json();

return (
<div className="py-12">
{/* Title section */}
<div className="text-center mb-12">
<h1 className="text-5xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Welcome to My Website
</h1>
<p className="text-gray-500 text-lg mt-4">
Built with Next.js + Tailwind CSS
</p>
</div>

{/* Data cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-4xl mx-auto">
<StatCard title="Users" value={stats.users} />
<StatCard title="Posts" value={stats.posts} />
<StatCard title="Comments" value={stats.comments} />
</div>
</div>
);
}

function StatCard({ title, value }: { title: string; value: number }) {
return (
<div className="bg-white rounded-xl shadow p-6 text-center hover:shadow-lg transition-shadow">
<p className="text-gray-500 text-sm">{title}</p>
<p className="text-3xl font-bold text-blue-600 mt-2">{value}</p>
</div>
);
}

Clean, clear, readable. AI generates this kind of code quickly and with excellent quality.


Other Frameworks Worth Knowing

Nuxt.js (Vue's answer to Next.js): If you prefer Vue, Nuxt is the go-to. Its philosophy is nearly identical to Next.js — full-stack, file-based routing, SSR. Vue's template syntax feels more intuitive to some people. But overall ecosystem and AI support lag behind Next.js.

SvelteKit: Svelte is a "compile-time framework" — your code gets compiled into vanilla JS at build time, with virtually zero framework overhead at runtime. SvelteKit is its full-stack counterpart. Extremely clean to write with outstanding performance. Ideal for projects chasing peak performance, though the ecosystem is relatively niche.

Astro: Specializes in content-driven websites (blogs, documentation sites, marketing pages). Its killer feature is zero JavaScript by default — pages output pure HTML, and JS only loads where interactivity is needed. If your site is primarily about showcasing content, Astro offers the best performance.

How to choose?

FrameworkEcosystemAI SupportBest For
Next.js⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐General web apps, top pick
Nuxt.js⭐⭐⭐⭐⭐⭐⭐Teams that prefer Vue
SvelteKit⭐⭐⭐⭐⭐⭐Chasing peak performance
Astro⭐⭐⭐⭐⭐⭐Content/documentation sites

When Shouldn't You Use Next.js?

Next.js is powerful, but not universal. These scenarios might call for something else:

1. Very simple static pages — Just a landing page with a few images, no dynamic content. Write plain HTML + Tailwind and deploy with Vercel or Netlify — no need to bring in all of Next.js.

2. Internal tools / admin dashboards — No SEO needed, no SSR needed. Vite + React is lighter and faster.

3. Pure content / blog sites — Astro or Hugo might be a better fit. They output pure HTML with crushing performance.

4. Mobile apps — Next.js is a web framework. For apps, use React Native, Flutter, or Swift/Kotlin.

5. Learning purposes — If you want to deeply understand React itself, start with Vite + React without a framework, grasp the fundamentals, then adopt a framework.


Summary

A framework isn't a religion — it's a tool. But the right tool makes a huge difference:

  • Beginners: Next.js + Tailwind CSS is the "standard answer" for 2024–2025
  • Core principle: Server Components first; only use Client Components when you need interactivity
  • AI era: Mainstream frameworks = higher quality and accuracy from AI-generated code
  • Don't overthink it: Pick one that feels right and start building — that's what actually matters