Setting Up
Project Setup
Scaffold a Next.js 14 app with TypeScript, understand the App Router folder structure, configure path aliases, and get a running local dev server showing your first page. By the end of this chapter the skeleton of osztromok.com is alive and running locally.
Chapter milestone: A Next.js 14 TypeScript project is running at
http://localhost:3000
with a custom home page, proper folder layout, and environment variables ready for the database connection added in Chapter 5.
App Router vs Pages Router — pick the right one
Next.js has two routing systems and the documentation covers both, which can be confusing. This course uses the App Router (introduced in Next.js 13, stable in 14). It is the current recommended approach and is what you will see in all new projects.
| Feature | Pages Router (old) | App Router (this course ✓) |
|---|---|---|
| Folder | pages/ |
app/ |
| Data fetching | getServerSideProps, getStaticProps |
async Server Components — just await in the component |
| Layouts | Manual, per-page _app.tsx |
Nested layout.tsx files — compose automatically |
| Default render | Client-side unless you opt into SSR/SSG | Server by default, opt into client with 'use client' |
| Status | Maintained but not recommended for new projects | Current recommended approach |
pages/ folder and functions like getServerSideProps.
Don't mix the two — the patterns are not compatible. This course is 100% App Router.
create-next-app
create-next-app is Next.js's official scaffolding tool — equivalent to
Spring Initializr if you're coming from Java. Run it once and it sets up everything:
TypeScript, ESLint, Tailwind, and the correct folder structure.
$ npx create-next-app@latest osztromok --typescript --tailwind --eslint --app --src-dir=false --import-alias="@/*" ✔ Would you like to use TypeScript? … Yes ✔ Would you like to use ESLint? … Yes ✔ Would you like to use Tailwind CSS? … Yes ✔ Would you like to use `src/` directory? … No ✔ Would you like to use App Router? … Yes ✔ Would you like to customize the import alias (@/*)? … Yes ✓ Creating a new Next.js app in /osztromok Installing dependencies… ✓ Done! Created osztromok $ cd osztromok $ npm run dev ▲ Next.js 14.x - Local: http://localhost:3000 - ready in 1234 ms
Open http://localhost:3000 in your browser — you should see the Next.js welcome page. That's it: you have a running app. Now let's understand what was created.
--typescript adds TS config & types ·
--tailwind installs and configures Tailwind CSS ·
--app uses the App Router ·
--src-dir=false puts files at the root (simpler) ·
--import-alias="@/*" lets you write @/components/Foo instead of
../../components/Foo.
What was created
Here is the generated structure annotated for the osztromok.com context. Files in green are created by the scaffold. Files in amber we will add ourselves in this chapter.
app/ as your
controllers layer, components/ as your view templates (but reactive),
and lib/ as your service/repository layer.
layout.tsx is your master template — everything else renders inside it.
The three files you touch every day
app/layout.tsx — the root layout. Rendered once, wraps every page. This is where the <html> and <body> tags live, and where you add nav and footer.
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: {
template: '%s | osztromok.com', // e.g. "Japanese | osztromok.com"
default: 'osztromok.com',
},
description: 'Philip\'s learning site',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<main>{children}</main>
</body>
</html>
)
}
app/page.tsx — the home page, rendered at /. Replace the default scaffold content with something real:
export default function HomePage() {
return (
<div className="min-h-screen bg-gray-950 text-white">
<h1 className="text-4xl font-bold">
osztromok.com
</h1>
<p className="text-gray-400 mt-4">
Coming soon — Next.js rebuild in progress.
</p>
</div>
)
}
next.config.js — tweak the defaults. For now, just add images config so we can serve images from the database host later:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'osztromok.com',
},
],
},
}
module.exports = nextConfig
TypeScript in 5 minutes (for Java developers)
TypeScript is JavaScript with types added. If you know Java, you already understand the concepts — the syntax is just different. Here are the patterns you'll see constantly:
// ── 1. Type annotations ─────────────────────────────────
const name: string = 'osztromok'
const port: number = 3000
const live: boolean = false
// ── 2. Interfaces (like Java interfaces / records) ───────
interface Subject {
id: number
name: string
slug: string
position: number
}
// ── 3. Optional fields (? means can be undefined) ───────
interface Page {
id: number
title: string
description: string | null // union type — string OR null
publishedAt: Date
subtopicId: number
}
// ── 4. Function with typed params + return type ──────────
function slugify(text: string): string {
return text.toLowerCase().replace(/\s+/g, '-')
}
// ── 5. Async function (same as Java's CompletableFuture) ─
async function getSubject(slug: string): Promise<Subject | null> {
// await pauses until the Promise resolves
const result = await fetchFromDB(slug)
return result ?? null // ?? = nullish coalescing
}
// ── 6. Array type ─────────────────────────────────────────
const subjects: Subject[] = []
// ── 7. Type inference — TS figures it out automatically ──
const count = subjects.length // inferred as number, no annotation needed
Java → TypeScript quick map
interface Foo {}→ same syntax ✓String→string(lowercase)int / long→numberboolean→boolean(same)List<T>→T[]orArray<T>Optional<T>→T | nullvoid→void(same)@Override→ TypeScript enforces via interface (no decorator needed)
Key differences from Java
- Types are structural, not nominal — if it has the same shape, it fits the type
- No classes required — plain objects and functions are idiomatic
undefinedandnullare separate —string | null | undefined- Type inference is strong — you rarely need to annotate every variable
- Compiled away at runtime — types exist only at dev time
anyexists but should be avoided (it turns off type checking)
tsconfig.json
The scaffold generates a good tsconfig.json. Verify it has these key settings —
create-next-app sets them correctly so you likely don't need to change anything,
but it helps to know what they mean:
{
"compilerOptions": {
"target": "ES2017", // compile to modern JS
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, // allow .js files alongside .ts
"skipLibCheck": true, // skip type-checking node_modules
"strict": true, // ← enable ALL strict checks (recommended)
"noEmit": true, // Next.js handles the actual compilation
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve", // Next.js transforms JSX itself
"incremental": true, // cache build info → faster rebuilds
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./*"] // @/components/Nav → ./components/Nav
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
@/* path alias is very useful. Once set, instead of
import Nav from '../../../components/Nav' you write
import Nav from '@/components/Nav' — it always resolves from the project root,
regardless of how deeply nested the importing file is.
Setting up .env.local
Create .env.local in the project root. This file is never committed to git
(it is already in .gitignore). It holds secrets and config that change between
dev and production.
# Database — filled in properly in Chapter 5
DATABASE_URL="mysql://root:password@localhost:3306/osztromok"
# NextAuth — filled in properly in Chapter 8
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="generate-this-with: openssl rand -base64 32"
# Admin credentials (hashed password added in Ch 8)
ADMIN_USERNAME="philip"
Next.js automatically loads .env.local in development. Variables are available server-side via process.env.DATABASE_URL. To expose a variable to the browser (client components), prefix it with NEXT_PUBLIC_ — but never put secrets there.
// Server Component or API route — works fine
const dbUrl = process.env.DATABASE_URL // ✓ server-side only
// Public variable — accessible in browser too
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL // ✓ safe to expose
// Guard against undefined (good practice)
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL is not set')
}
Add components/, lib/, and types/
The scaffold only creates app/ and public/.
Add the folders we'll use throughout the course:
$ mkdir components lib types # Create placeholder files so the folders are tracked by git $ touch components/.gitkeep lib/.gitkeep types/index.ts
Then create a minimal shared types file — we'll add to this as the course progresses:
// Shared TypeScript interfaces for osztromok.com
// These mirror the database schema — expanded in Chapter 5 with Prisma
export interface Subject {
id: number
name: string
slug: string
position: number
}
export interface Subtopic {
id: number
name: string
slug: string
position: number
subjectId: number
}
export interface Page {
id: number
title: string
slug: string
description: string | null
subtopicId: number
}
export interface PageContent {
id: number
heading: string
body: string
position: number
pageId: number
}
Server Components vs Client Components
This is the most important concept in the App Router — and the most confusing if you come from the old Next.js or standard React. Here's the mental model:
Server Components (default)
Rendered on the server. Never sent to the browser as JavaScript. Can do anything a Node.js program can do — database queries, file reads, secrets.
- No
'use client'at the top → Server Component - Can
awaitdatabase calls directly in the component - Cannot use
useState,useEffect, event handlers - Cannot access browser APIs (
window,localStorage) - Most of our components will be Server Components
Client Components ('use client')
Rendered on the server first (for SEO), then hydrated in the browser. Have access to React hooks and browser APIs. Sent to the browser as JS.
- Add
'use client'at the very top of the file - Can use
useState,useEffect,onClick - Cannot
awaitat the component's top level - Use for: nav toggle, forms with state, rich-text editor
- Only add 'use client' when you actually need interactivity
'use client'.
Only add it when you get an error saying you're using a hook or browser API in a Server Component.
This keeps your app fast — less JavaScript shipped to the browser.
Commands you'll use every day
# Start the development server (hot reload on save) $ npm run dev # Type-check without building $ npx tsc --noEmit # Build for production (checks for errors) $ npm run build # Run the production build locally $ npm start # Add a package (example: Prisma in Chapter 5) $ npm install prisma @prisma/client
VS Code extensions to install
- ESLint — shows errors inline as you type
- Prettier — auto-formats on save
- Tailwind CSS IntelliSense — autocomplete for class names
- Prisma — syntax highlighting for schema.prisma (Ch 5)
- ES7+ React/Redux/React-Native — useful snippets
Hot reload — how it works
npm run dev starts a watcher. When you save any .tsx
or .ts file, the browser updates automatically — no manual refresh needed.
Changes to layout.tsx reload the whole page; changes inside a component
update just that component (Fast Refresh) without losing state.
What you should have right now
Next.js 14 app scaffolded
Running npm run dev shows something at http://localhost:3000 — even if it's just the Next.js welcome page for now.
Custom home page in app/page.tsx
The default scaffold content replaced with your own placeholder for osztromok.com.
Folder structure ready
components/, lib/, and types/ folders created. types/index.ts has the four core interfaces.
.env.local created (not committed)
Placeholder values for DATABASE_URL, NEXTAUTH_URL, and NEXTAUTH_SECRET. Confirmed .gitignore includes .env.local.
TypeScript compiles cleanly
Run npx tsc --noEmit — zero errors.
http://localhost:3000 is taken, Next.js automatically tries 3001, 3002, etc.
and tells you in the terminal. Or pass npm run dev -- --port 3001 to force a port.