Skip to main content
Next.js Tutorial

Complete Next.js Project Setup: From Installation to AI Integration

Create a maintainable Next.js 16 foundation with TypeScript, Git, linting, formatting, Tailwind CSS v4, optimized assets, metadata, secure configuration, and an AI interviewer starting point.

Modern development workspace connected to project architecture, security, assets, and AI services

Creating a Next.js application takes one command. Preparing it for a real product requires more deliberate choices. Version control, consistent formatting, design tokens, metadata, asset conventions, secret management, and server boundaries are inexpensive at the beginning and costly to retrofit after a project grows.

This tutorial creates an AI interviewer foundation, but the same structure works for SaaS products, dashboards, portfolios, blogs, and client applications. The AI portion stops at a secure, validated API boundary. That is intentional: establish a reliable application before adding a provider, authentication, usage limits, and paid model calls.

What we are building

Setup flow
Create Next.js application
  -> Configure Git and GitHub
  -> Verify ESLint and add Prettier
  -> Define Tailwind CSS theme tokens
  -> Configure fonts and assets
  -> Add metadata and environment variables
  -> Create validated interview API boundary
  -> Run production checks

The project separates routes, interface components, reusable logic, types, static assets, and server-only integrations. This keeps a feature from spreading unrelated code across the application.

1. Create the Next.js 16 application

Install Node.js 20.9 or newer, confirm it with node --version, then run the official project generator:

Terminal
npx create-next-app@latest ai-interviewer
cd ai-interviewer
npm run dev

The recommended defaults currently enable TypeScript, ESLint, Tailwind CSS, the App Router, and Turbopack. If you customize the prompts, keep TypeScript, ESLint, Tailwind CSS, and the App Router enabled. A src directory is an organizational preference; either choice works when used consistently.

Visit http://localhost:3000. A new application contains an app directory for routes and layouts, public for static assets, eslint.config.mjs for lint rules, next.config.ts for framework settings, tsconfig.json for TypeScript, and package.json for scripts and dependencies.

Initial structure
ai-interviewer/
|-- app/
|   |-- globals.css
|   |-- layout.tsx
|   `-- page.tsx
|-- public/
|-- eslint.config.mjs
|-- next.config.ts
|-- package.json
|-- postcss.config.mjs
`-- tsconfig.json

2. Configure Git and GitHub

Version control should begin before feature work. create-next-app normally initializes Git when the command is available; check the repository before running git init again.

Initial commit
git status
git add .
git commit -m "Initial Next.js project setup"

Create an empty GitHub repository, then connect and push it. Replace the example account name with your own:

Connect remote
git remote add origin https://github.com/USERNAME/ai-interviewer.git
git branch -M main
git push -u origin main

Use short-lived branches for changes that need review:

Feature branch
git switch -c feature/theme
# make and test changes
git add .
git commit -m "Add application theme"
git push -u origin feature/theme

3. Configure ESLint and Prettier

ESLint reports likely bugs and framework-specific problems. Next.js 16 uses the ESLint CLI; next lint was removed, and next build no longer runs lint automatically. Keep a separate lint command in local development and continuous integration.

eslint.config.mjs
import { defineConfig, globalIgnores } from "eslint/config"
import nextVitals from "eslint-config-next/core-web-vitals"
import nextTs from "eslint-config-next/typescript"
import prettier from "eslint-config-prettier/flat"

export default defineConfig([
  ...nextVitals,
  ...nextTs,
  prettier,
  globalIgnores([
    ".next/**",
    "out/**",
    "build/**",
    "next-env.d.ts",
  ]),
])

Prettier handles formatting. The Tailwind plugin sorts utility classes consistently:

Terminal
npm install --save-dev prettier prettier-plugin-tailwindcss eslint-config-prettier
.prettierrc
{
  "semi": true,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "plugins": ["prettier-plugin-tailwindcss"]
}
.prettierignore
.next
node_modules
dist
build
coverage

Add scripts for checks without replacing the scripts generated by Next.js:

package.json scripts
"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "lint": "eslint .",
  "format": "prettier --write .",
  "format:check": "prettier --check ."
}

4. Configure Tailwind CSS v4 and design tokens

When Tailwind is selected during setup, app/globals.css imports the framework. Tailwind CSS v4 supports CSS-first design tokens through @theme:

app/globals.css
@import "tailwindcss";

@theme {
  --font-sans: var(--font-geist);
  --color-brand-500: #6366f1;
  --color-brand-600: #4f46e5;
}

Application colors should describe purpose rather than a one-off shade. Keep raw values in CSS variables, then consume them consistently across components:

Theme variables
:root {
  --background: #ffffff;
  --foreground: #111827;
  --card: #f8fafc;
  --primary: #6366f1;
}

.dark {
  --background: #020617;
  --foreground: #f8fafc;
  --card: #0f172a;
  --primary: #818cf8;
}

A provider can apply Light, Dark, or System at the document root. See the complete Next.js light and dark theme guide for the provider, accessible selector, and hydration-safe setup.

5. Add optimized fonts

next/font downloads and self-hosts font files during the build, reducing external requests and layout shift. Define the variable once and attach it to the document body:

app/layout.tsx
import { Geist } from "next/font/google"
import "./globals.css"

const geist = Geist({
  subsets: ["latin"],
  variable: "--font-geist",
})

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en">
      <body className={geist.variable}>{children}</body>
    </html>
  )
}

6. Organize images and static assets

Files in public are served from the site root. Store assets by purpose and use lowercase, descriptive filenames:

Public files
public/
|-- images/
|   |-- hero.webp
|   |-- dashboard.webp
|   `-- interview.webp
|-- icons/
|   `-- logo.svg
`-- videos/
    `-- demo.mp4

public/images/hero.webp is available as /images/hero.webp. Prefer next/image for content images because dimensions prevent layout shift and the framework can resize and optimize the file:

Optimized image
import Image from "next/image"

<Image
  src="/images/hero.webp"
  alt="AI interview dashboard showing practice feedback"
  width={1200}
  height={700}
  sizes="(max-width: 768px) 100vw, 1200px"
/>

7. Configure metadata and SEO

The App Router supports a typed static metadata object in Server Components. Set a title template, useful description, canonical base, and social-sharing fields early:

app/layout.tsx metadata
import type { Metadata } from "next"

export const metadata: Metadata = {
  metadataBase: new URL("https://example.com"),
  title: {
    default: "AI Interviewer",
    template: "%s | AI Interviewer",
  },
  description: "Practice technical interviews with structured feedback.",
  alternates: { canonical: "/" },
  openGraph: {
    title: "AI Interviewer",
    description: "Practice technical interviews with structured feedback.",
    type: "website",
    images: ["/opengraph-image.jpg"],
  },
}

Add route-specific metadata when a page has a different purpose. Next.js also supports file conventions for favicons, Open Graph images, robots.txt, and sitemaps. Metadata supports discoverability and clear sharing previews; it does not replace useful visible content.

8. Protect environment variables

Never put a secret directly in source code or a Client Component. Keep local values in .env.local, which should already be ignored by the default project template:

.env.local
OPENAI_API_KEY=your_api_key
DATABASE_URL=your_database_url
NEXT_PUBLIC_APP_URL=http://localhost:3000

Only NEXT_PUBLIC_APP_URL is intended for the browser. Variables beginning with NEXT_PUBLIC_ are inlined into client JavaScript at build time. An AI key, database password, signing secret, or private token must never use that prefix. Configure secrets separately in the deployment platform and rotate a key immediately if it is committed or exposed.

OpenAI key safety: the official SDK reads OPENAI_API_KEY from the server environment. Do not send that value to the browser, place it in public files, print it in logs, or include it in an error response.

9. Create a validated interview API route

Start with an endpoint that establishes the server boundary. It validates content and size but does not call a paid AI model yet:

app/api/interview/route.ts
import { NextResponse } from "next/server"

type InterviewRequest = {
  question?: unknown
  answer?: unknown
}

export async function POST(request: Request) {
  try {
    const body = (await request.json()) as InterviewRequest
    const question = typeof body.question === "string" ? body.question.trim() : ""
    const answer = typeof body.answer === "string" ? body.answer.trim() : ""

    if (!question || !answer) {
      return NextResponse.json(
        { error: "Question and answer are required." },
        { status: 400 },
      )
    }

    if (question.length > 500 || answer.length > 5000) {
      return NextResponse.json(
        { error: "The submitted content is too long." },
        { status: 413 },
      )
    }

    return NextResponse.json({
      message: "Answer received successfully.",
      answerLength: answer.length,
    })
  } catch {
    return NextResponse.json(
      { error: "Invalid JSON request." },
      { status: 400 },
    )
  }
}

Before connecting a provider, add authentication, authorization, rate limits, abuse controls, timeouts, schema validation, safe logging, and a clear data-retention policy. A server route hides the key from the browser, but it does not automatically make an endpoint secure.

10. Build the interview form

The form needs state and an event handler, so it is a Client Component. Keep the rest of the page server-rendered.

components/interview/interview-form.tsx
"use client"

import { FormEvent, useState } from "react"

export function InterviewForm() {
  const [answer, setAnswer] = useState("")
  const [message, setMessage] = useState("")
  const [pending, setPending] = useState(false)

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setPending(true)
    setMessage("")

    try {
      const response = await fetch("/api/interview", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          question: "Explain React Server Components.",
          answer,
        }),
      })
      const data = await response.json()
      if (!response.ok) throw new Error(data.error ?? "Request failed.")
      setMessage(data.message)
    } catch (error) {
      setMessage(error instanceof Error ? error.message : "Request failed.")
    } finally {
      setPending(false)
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <label htmlFor="answer" className="text-xl font-semibold">
        Explain React Server Components.
      </label>
      <textarea id="answer" value={answer} required maxLength={5000}
        onChange={(event) => setAnswer(event.target.value)}
        className="min-h-40 w-full rounded-xl border p-4" />
      <button disabled={pending} className="rounded-lg bg-black px-5 py-3 text-white disabled:opacity-60">
        {pending ? "Submitting..." : "Submit answer"}
      </button>
      <p role="status" aria-live="polite">{message}</p>
    </form>
  )
}

11. Final architecture and folder structure

Recommended structure
ai-interviewer/
|-- app/
|   |-- api/interview/route.ts
|   |-- interview/page.tsx
|   |-- globals.css
|   |-- layout.tsx
|   `-- page.tsx
|-- components/
|   |-- interview/interview-form.tsx
|   |-- layout/navbar.tsx
|   `-- ui/
|-- lib/
|   |-- ai.ts
|   |-- utils.ts
|   `-- validations.ts
|-- types/interview.ts
|-- public/
|   |-- images/
|   |-- icons/
|   `-- videos/
|-- .env.local
|-- .gitignore
|-- .prettierrc
|-- eslint.config.mjs
|-- next.config.ts
|-- package.json
`-- tsconfig.json

The browser owns forms, interaction, local state, and accessible feedback. Server Components own non-interactive page composition. Route Handlers validate requests and coordinate authentication, databases, and external APIs. Provider credentials stay in server-only modules.

12. Common setup mistakes

  • Putting everything in page.tsx: extract coherent features, but avoid creating tiny components without a reuse or clarity benefit.
  • Adding use client everywhere: keep the client boundary close to the interaction that needs it.
  • Exposing credentials: never use NEXT_PUBLIC_ for private keys and never return secrets in API errors.
  • Trusting TypeScript as runtime validation: browser requests are untrusted even when your local interface is typed.
  • Assuming build includes lint: Next.js 16 requires a separate ESLint command.
  • Skipping states: design loading, success, empty, validation, network-error, and unauthorized experiences.
  • Overengineering folders: create boundaries as features appear; empty architecture is not useful architecture.

13. Production checklist

  • TypeScript checks and npm run build complete without errors.
  • npm run lint and npm run format:check pass separately.
  • Deployment environment variables are configured and no secrets are committed.
  • Images have dimensions, responsive sizes, and appropriate alternative text.
  • Titles, descriptions, canonical URLs, social images, sitemap, and robots rules are correct.
  • Forms work with keyboard and screen readers and expose useful status messages.
  • API endpoints authenticate users, validate input, authorize actions, and rate-limit expensive work.
  • Mobile layouts, error paths, a production build, and a production-like server are tested.
Final local checks
npm run lint
npm run format:check
npm run build
npm run start

14. Using Codex while building

AI coding tools produce better results when the request names the architecture and verification rules. Ask the tool to inspect the existing repository first, preserve unrelated files, keep secrets server-side, and run the project's actual checks.

Example Codex prompt
Review my existing Next.js project before making changes.

Stack: Next.js App Router, TypeScript, Tailwind CSS v4.
Prefer Server Components. Use Client Components only for interaction.
Follow the existing structure and do not create duplicate components.
Keep API keys server-side. Use semantic HTML and accessible controls.

Create an AI interviewer foundation with:
1. Interview page and form
2. Validated API route
3. TypeScript types
4. Loading and error states
5. Relevant lint, type, and build checks

Before editing, list the files you expect to create or modify.

Generated code remains a draft. Review imports, package choices, security assumptions, data handling, and version-specific APIs before shipping it.

Frequently asked questions

What Node.js version does Next.js 16 require?

Next.js 16 requires Node.js 20.9 or newer. Check the installed version with node --version before creating the project.

Does next build run ESLint in Next.js 16?

No. Next.js 16 removed the automatic lint step from next build. Run the ESLint CLI separately in local checks and continuous integration.

Should an AI API key use the NEXT_PUBLIC prefix?

No. Secret AI credentials must remain server-side. NEXT_PUBLIC variables are included in browser JavaScript and must contain only intentionally public values.

Should every component use the use client directive?

No. Components are server-rendered by default in the App Router. Add use client only at boundaries that need state, event handlers, effects, browser APIs, or client-only libraries.

Is the example interview route connected to an AI model?

No. It demonstrates a validated server endpoint and client form. A provider SDK, authentication, rate limiting, safety controls, and model call can be added after the foundation is tested.

Official references

Conclusion and next steps

A production-minded setup is not a large collection of tools. It is a set of clear boundaries: code is versioned, formatting is predictable, linting runs explicitly, visual decisions use tokens, assets have conventions, metadata describes each route, and secrets never cross into the browser.

The AI interviewer now has a safe starting boundary rather than a pretend integration. The next stages are authentication, database design, interview creation, provider integration, streaming feedback, usage controls, evaluation quality, reporting, and history. Add each capability behind validation, authorization, and tests instead of generating every feature at once.

Planning a Next.js application?

NavTech Solution can help design a maintainable, accessible, secure foundation for your product.

Discuss your project