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
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 checksThe 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:
npx create-next-app@latest ai-interviewer
cd ai-interviewer
npm run devThe 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.
ai-interviewer/
|-- app/
| |-- globals.css
| |-- layout.tsx
| `-- page.tsx
|-- public/
|-- eslint.config.mjs
|-- next.config.ts
|-- package.json
|-- postcss.config.mjs
`-- tsconfig.json2. 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.
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:
git remote add origin https://github.com/USERNAME/ai-interviewer.git
git branch -M main
git push -u origin mainUse short-lived branches for changes that need review:
git switch -c feature/theme
# make and test changes
git add .
git commit -m "Add application theme"
git push -u origin feature/theme3. 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.
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:
npm install --save-dev prettier prettier-plugin-tailwindcss eslint-config-prettier{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "all",
"plugins": ["prettier-plugin-tailwindcss"]
}.next
node_modules
dist
build
coverageAdd scripts for checks without replacing the scripts generated by Next.js:
"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:
@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:
: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:
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/
|-- images/
| |-- hero.webp
| |-- dashboard.webp
| `-- interview.webp
|-- icons/
| `-- logo.svg
`-- videos/
`-- demo.mp4public/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:
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:
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:
OPENAI_API_KEY=your_api_key
DATABASE_URL=your_database_url
NEXT_PUBLIC_APP_URL=http://localhost:3000Only 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_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:
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.
"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
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.jsonThe 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 buildcomplete without errors. npm run lintandnpm run format:checkpass 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.
npm run lint
npm run format:check
npm run build
npm run start14. 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.
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
- Next.js installation and tooling
- Next.js ESLint configuration
- Next.js production checklist
- Tailwind CSS directives and @theme
- OpenAI developer quickstart and API key setup
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
