Environment, App Router & How Next.js Actually Works
Environment, App Router & How Next.js Actually Works
Why Next.js? The Problem with Plain React
A plain React app (from Vite) ships an empty HTML shell to the browser:
<div id="root"></div>
<script src="/bundle.js"></script>The browser downloads the JS bundle, executes it, then React renders your app into #root. Until the JS runs, the user sees nothing. Search engines see nothing. This is called Client-Side Rendering (CSR) and it has real costs:
- Slow First Contentful Paint (FCP) — users wait for JS before seeing content
- Poor SEO — crawlers often don't execute JS
- No direct database access — everything needs a separate API endpoint
Next.js adds Server-Side Rendering (SSR), Static Site Generation (SSG), and Incremental Static Regeneration (ISR) — giving you pre-rendered HTML from the server on every request, at build time, or on a schedule. Users get fast, SEO-friendly pages. The App Router takes this further: components run on the server by default and you opt into client behavior only when needed.
Setting Up Your Environment
Step 1 — Create the project:
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
npm run devFlags explained:
--typescript: TypeScript from the start (industry standard)--tailwind: Tailwind CSS pre-configured--app: App Router (not the older Pages Router)
Your app runs at http://localhost:3000. The terminal shows a build output with routes detected. HMR works — save a file and the browser updates instantly.
Step 2 — VS Code extensions to install:
- Next.js extension (by Vercel) or the ES7+ React snippets extension: Component scaffolding shortcuts.
- Tailwind CSS IntelliSense: Autocomplete for Tailwind class names in JSX.
- Prettier: Format on save.
Step 3 — Understand the start-up output:
Run npm run dev and you will see something like:
▲ Next.js 15.x.x
- Local: http://localhost:3000
- Environments: .env.local
✓ Starting...
✓ Ready in 2.1sNext.js is running a Node.js server that handles every request — running your server components, serving static files, and proxying to the Next.js runtime.
File System Routing — The Mental Model
Every page.tsx file inside src/app/ defines a route. The folder path becomes the URL path:
src/app/
layout.tsx ← root layout — html, body, global providers
page.tsx ← / (home page)
about/
page.tsx ← /about
courses/
layout.tsx ← wraps all /courses/* routes (shared nav)
page.tsx ← /courses (course list)
[id]/
page.tsx ← /courses/cc-js-m01, /courses/cc-react-m02, etc.
loading.tsx ← shown as Suspense fallback while page loads
error.tsx ← shown if the page component throws
api/
courses/
route.ts ← API endpoint: GET/POST /api/coursesThis is the entire routing system. No router configuration file. No <Route path="...">. The file system IS the router.
Server Components vs Client Components
This is the most important mental model in Next.js 15.
Server Component (default — no directive needed):
// src/app/courses/page.tsx
// No "use client" = Server Component
// Runs on the server. Can async/await. Ships ZERO JavaScript to the browser.
import { getCourses } from '@/lib/courses'
export default async function CoursesPage() {
const courses = await getCourses() // direct function call — no API needed
return (
<main>
<h1>All Courses</h1>
<ul>
{courses.map(c => (
<li key={c.id}>{c.title} — {c.xp} XP</li>
))}
</ul>
</main>
)
}No useEffect. No loading state. No API route needed. The server calls getCourses() directly, gets the data, renders HTML, sends it to the browser. The browser receives fully-rendered HTML with the course list — instant paint.
Client Component (opt-in):
'use client' // ← this one directive changes everything
import { useState } from 'react'
export function SearchBar({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('') // useState works now
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
onKeyDown={e => e.key === 'Enter' && onSearch(query)}
placeholder="Search courses..."
/>
)
}'use client' marks a component (and all its imports) as client code. This JS is bundled and shipped to the browser. Use it only for: hooks, event handlers, browser APIs (localStorage, window, geolocation).
The Rule: Push 'use client' As Deep As Possible
// WRONG — entire page is client-side for one interactive element
'use client'
export default async function CoursesPage() { ... }
// CORRECT — only the search bar is client-side
// src/app/courses/page.tsx (Server Component)
import { SearchBar } from '@/components/SearchBar' // 'use client' inside there
export default async function CoursesPage() {
const courses = await getCourses() // server-only logic still works
return (
<main>
<SearchBar /> {/* ← client island */}
<CourseList courses={courses} /> {/* ← server-rendered */}
</main>
)
}Think of your page as mostly server-rendered HTML with small "islands" of client interactivity.
Root Layout — The App Shell
// src/app/layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'JST Academy',
description: 'PhD-level crash courses for modern developers',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>
<nav>JST Academy</nav>
<main>{children}</main>
<footer>© 2026 J Supreme Tech</footer>
</body>
</html>
)
}This layout wraps every page in the app. The metadata export auto-generates <title> and <meta description> tags. children is the current page's rendered output.