Setting Up

Chapter 1 — Project Setup | Next.js Course
Next.js Rebuild Course Chapter 1 of 10

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
If you see Pages Router examples online: the key giveaway is 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.

Terminal
$ 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.

Flags explained: --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.

osztromok/ ├── app/ ← App Router lives here (replaces pages/) │ ├── layout.tsx ← root layout: html, body, nav, footer │ ├── page.tsx ← home page "/" │ ├── globals.css ← global styles (imported in layout.tsx) │ └── favicon.ico │ ├── public/ ← static files served at "/" (images, robots.txt) │ ├── components/ ← create this: reusable React components │ ├── Nav.tsx │ └── Footer.tsx │ ├── lib/ ← create this: shared utilities, DB client │ └── prisma.ts ← added in Chapter 5 │ ├── types/ ← create this: shared TypeScript interfaces │ └── index.ts │ ├── tailwind.config.ts ← Tailwind customisation (colours, fonts) ├── tsconfig.json ← TypeScript compiler config ├── next.config.js ← Next.js config (image domains, rewrites etc.) ├── .eslintrc.json ├── .env.local ← secrets: DATABASE_URL, NEXTAUTH_SECRET (never commit) └── package.json
Coming from Java/Spring Boot: think of 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.

app/layout.tsx TypeScript · Server Component
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:

app/page.tsx TypeScript · Server Component
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:

next.config.js JavaScript
/** @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:

TypeScript essentials TypeScript
// ── 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 ✓
  • Stringstring (lowercase)
  • int / longnumber
  • booleanboolean (same)
  • List<T>T[] or Array<T>
  • Optional<T>T | null
  • voidvoid (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
  • undefined and null are 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
  • any exists 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:

tsconfig.json JSON
{
  "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"]
}
The @/* 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.

.env.local Environment · never commit this file
# 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.

Reading env vars in server code TypeScript
// 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:

Terminal — in the osztromok/ folder
$ 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:

types/index.ts TypeScript
// 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 await database 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 await at the component's top level
  • Use for: nav toggle, forms with state, rich-text editor
  • Only add 'use client' when you actually need interactivity
Rule of thumb: start every component without '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

Terminal
# 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.

Troubleshooting — port already in use: If 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.