Environment Setup — create-next-app, Folder Structure, next.config.js Key Settings
Creating a Project: create-next-app
Initialize a project using create-next-app, the official Next.js CLI tool.
npx create-next-app@latest my-app
Running this displays an interactive prompt:
What is your project named? my-app
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 your code inside a `src/` directory? › No
Would you like to use App Router? (recommended) › Yes
Would you like to use Turbopack for `next dev`? › Yes
Would you like to customize the import alias (@/* by default)? › No
Recommended settings: Choose Yes for TypeScript, ESLint, and App Router. Choose Tailwind CSS based on your project's needs.
Create with a Template
# Use an example template
npx create-next-app@latest my-app --example with-tailwindcss
npx create-next-app@latest my-app --example blog-starter
# Pin to a specific version
npx create-next-app@15.0.0 my-app
Add Next.js to an Existing Project
npm install next@latest react@latest react-dom@latest
Add scripts to package.json:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}
Generated Folder Structure
The default structure created by create-next-app:
my-app/
├── app/ # App Router root
│ ├── favicon.ico
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ └── page.tsx # Home page (/)
├── public/ # Static files (images, fonts, etc.)
│ ├── next.svg
│ └── vercel.svg
├── .eslintrc.json
├── .gitignore
├── next.config.ts # Next.js configuration
├── package.json
├── postcss.config.mjs # Tailwind CSS configuration
├── tailwind.config.ts # Tailwind customization
└── tsconfig.json # TypeScript configuration
Recommended Extended Folder Structure
A recommended structure for real production projects:
my-app/
├── app/
│ ├── (auth)/ # Auth-related Route Group
│ │ ├── login/
│ │ │ └── page.tsx
│ │ └── register/
│ │ └── page.tsx
│ ├── (main)/ # Main content Route Group
│ │ ├── layout.tsx # Main layout
│ │ ├── dashboard/
│ │ │ ├── page.tsx
│ │ │ └── loading.tsx
│ │ └── settings/
│ │ └── page.tsx
│ ├── api/ # Route Handlers
│ │ └── users/
│ │ └── route.ts
│ ├── globals.css
│ └── layout.tsx # Root layout
├── components/ # Reusable components
│ ├── ui/ # Pure UI components (shadcn/ui, etc.)
│ │ ├── Button.tsx
│ │ └── Input.tsx
│ └── features/ # Feature-based components
│ ├── auth/
│ └── dashboard/
├── lib/ # Utilities and helpers
│ ├── utils.ts
│ ├── db.ts # DB client
│ └── auth.ts # Auth helpers
├── hooks/ # Custom hooks
│ └── useLocalStorage.ts
├── types/ # TypeScript type definitions
│ └── index.ts
├── public/
└── next.config.ts
Using the src/ Directory
If you chose the src/ directory option:
my-app/
├── src/
│ ├── app/ # src/app/ instead of app/
│ ├── components/
│ └── lib/
├── public/
└── next.config.ts
Both approaches are supported. Using src/ keeps source code and configuration files neatly separated.
Key next.config.ts Settings
From Next.js 15, next.config.ts (TypeScript) is used by default.
Basic Structure
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// configuration options
};
export default nextConfig;
Key Configuration Options
1. Allowing Image Domains (images)
To optimize external images with the <Image> component, you must allow their domains.
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: '**.amazonaws.com', // wildcard subdomain
},
],
// Image optimization format priority
formats: ['image/avif', 'image/webp'],
// Minimum cache duration in seconds
minimumCacheTTL: 3600,
},
};
2. Exposing Environment Variables (env)
const nextConfig: NextConfig = {
env: {
// Variables exposed to the client (without needing NEXT_PUBLIC_)
APP_VERSION: process.env.npm_package_version,
BUILD_TIME: new Date().toISOString(),
},
};
Note: For server-only environment variables, access them as
process.env.MY_SECRETinside Server Components. Use theNEXT_PUBLIC_prefix to expose variables to the client.
3. Redirects (redirects)
const nextConfig: NextConfig = {
async redirects() {
return [
{
source: '/old-blog/:slug',
destination: '/blog/:slug',
permanent: true, // 301 (permanent), false = 307 (temporary)
},
{
source: '/docs',
destination: '/docs/introduction',
permanent: false,
},
// Conditional redirect
{
source: '/admin',
has: [
{
type: 'cookie',
key: 'auth-token',
value: undefined, // when cookie is absent
},
],
destination: '/login',
permanent: false,
},
];
},
};
4. Rewrites (rewrites)
Rewrites process a request as a different path without changing the URL. Useful for proxy configurations.
const nextConfig: NextConfig = {
async rewrites() {
return [
// Proxy /api/v1/* to an external API
{
source: '/api/v1/:path*',
destination: 'https://api.external.com/:path*',
},
// Support legacy URLs
{
source: '/old-products/:id',
destination: '/products/:id',
},
];
},
};
5. Security Headers (headers)
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
].join('; '),
},
],
},
{
// Add CORS headers to API routes
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
{ key: 'Access-Control-Allow-Methods', value: 'GET, POST, PUT, DELETE, OPTIONS' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
],
},
];
},
};
6. Bundle Analyzer Setup
// next.config.ts
import type { NextConfig } from 'next';
import bundleAnalyzer from '@next/bundle-analyzer';
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
const nextConfig: NextConfig = {
// ...
};
export default withBundleAnalyzer(nextConfig);
# Run bundle analysis
ANALYZE=true npm run build
7. Experimental Features (experimental)
const nextConfig: NextConfig = {
experimental: {
// Partial Prerendering (PPR)
ppr: 'incremental',
// React Compiler (automatic memoization)
reactCompiler: true,
// Type-safe Link
typedRoutes: true,
// File upload size limit for Server Actions
serverActions: {
bodySizeLimit: '10mb',
},
},
};
8. Output Configuration (output)
const nextConfig: NextConfig = {
// Standalone build (optimized for Docker deployment)
output: 'standalone',
// Or full static output
// output: 'export',
// trailingSlash: true, // Recommended with export mode
};
Managing Environment Variables
.env File Hierarchy
.env # All environments (committed to git)
.env.local # Local overrides (included in .gitignore)
.env.development # Development environment (npm run dev)
.env.production # Production (npm run build)
.env.test # Test environment
Priority: .env.local > .env.{NODE_ENV} > .env
Environment Variable Example
# .env.local
DATABASE_URL=postgresql://localhost:5432/mydb
API_SECRET_KEY=super-secret-key-here
# Exposed to client (NEXT_PUBLIC_ prefix required)
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
Type-safe Environment Variables in TypeScript
// lib/env.ts — environment variable validation
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
API_SECRET_KEY: z.string().min(32),
NEXT_PUBLIC_API_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'production', 'test']),
});
// Validate environment variables at build time
export const env = envSchema.parse(process.env);
TypeScript Configuration (tsconfig.json)
The default tsconfig.json generated by create-next-app:
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Useful Additional Settings
{
"compilerOptions": {
// ...base settings...
// Stricter type checking
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"exactOptionalPropertyTypes": true,
// Additional absolute path aliases
"paths": {
"@/*": ["./*"],
"@components/*": ["./components/*"],
"@lib/*": ["./lib/*"],
"@types/*": ["./types/*"]
}
}
}
ESLint Configuration
Next.js includes the next/core-web-vitals ruleset by default.
// .eslintrc.json
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
Adding Custom ESLint Rules
{
"extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
// Warn on console.log
"no-console": ["warn", { "allow": ["warn", "error"] }],
// Error on unused variables
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
// Warn on any type usage
"@typescript-eslint/no-explicit-any": "warn"
}
}
Practical Example: Complete Project Initial Setup
1. Create the Project
npx create-next-app@latest my-production-app \
--typescript \
--tailwind \
--eslint \
--app \
--turbopack \
--import-alias "@/*"
2. Install Additional Packages
cd my-production-app
# UI and styling
npm install clsx tailwind-merge
# Form handling
npm install react-hook-form @hookform/resolvers zod
# Data fetching (client-side)
npm install @tanstack/react-query
# Authentication
npm install next-auth@beta
# Database (e.g., Prisma + PostgreSQL)
npm install prisma @prisma/client
npm install -D @types/node
3. Complete next.config.ts Example
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// Image optimization
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
{
protocol: 'https',
hostname: '**.cloudinary.com',
},
],
formats: ['image/avif', 'image/webp'],
},
// Security headers
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
],
},
];
},
// Redirects
async redirects() {
return [
{
source: '/home',
destination: '/',
permanent: true,
},
];
},
// Experimental features
experimental: {
ppr: 'incremental',
reactCompiler: true,
typedRoutes: true,
},
// Standalone build for Docker deployment
output: 'standalone',
};
export default nextConfig;
4. Initial Root Layout Setup
// app/layout.tsx
import type { Metadata, Viewport } from 'next';
import { Inter, Noto_Sans_KR } from 'next/font/google';
import './globals.css';
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
});
const notoSansKR = Noto_Sans_KR({
subsets: ['latin'],
variable: '--font-noto-sans-kr',
display: 'swap',
});
export const metadata: Metadata = {
metadataBase: new URL('https://myapp.com'),
title: {
template: '%s | My App',
default: 'My App — The Best Service',
},
description: 'The best solution for your business',
keywords: ['Next.js', 'React', 'TypeScript'],
authors: [{ name: 'My Team', url: 'https://myapp.com' }],
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://myapp.com',
siteName: 'My App',
images: [
{
url: '/og-image.png',
width: 1200,
height: 630,
alt: 'My App OG Image',
},
],
},
twitter: {
card: 'summary_large_image',
site: '@myapp',
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
};
export const viewport: Viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#ffffff' },
{ media: '(prefers-color-scheme: dark)', color: '#0f172a' },
],
width: 'device-width',
initialScale: 1,
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="en"
className={`${inter.variable} ${notoSansKR.variable}`}
suppressHydrationWarning
>
<body className="min-h-screen bg-background font-sans antialiased">
{children}
</body>
</html>
);
}
5. Start the Development Server
# Start a fast dev server with Turbopack (default in Next.js 15)
npm run dev
# Access in browser
# http://localhost:3000
Developer Tooling Setup
VS Code Extensions
// .vscode/extensions.json
{
"recommendations": [
"bradlc.vscode-tailwindcss", // Tailwind IntelliSense
"esbenp.prettier-vscode", // Prettier formatter
"dbaeumer.vscode-eslint", // ESLint
"ms-vscode.vscode-typescript-next" // Latest TypeScript version
]
}
VS Code Settings
// .vscode/settings.json
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"tailwindCSS.experimental.classRegex": [
["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}
Prettier Configuration
// .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"]
}
Pro Tips
1. Clean Imports with Path Aliases
// paths setting in tsconfig.json
{
"paths": {
"@/*": ["./*"],
"@components/*": ["./components/*"],
"@lib/*": ["./lib/*"],
"@types/*": ["./types/*"],
"@hooks/*": ["./hooks/*"]
}
}
// Before: messy relative path import
import Button from '../../../components/ui/Button';
// After: clean absolute path import
import Button from '@components/ui/Button';
2. Config Composition Pattern
// next.config.ts
import type { NextConfig } from 'next';
// Combine config using a plugin pattern
function withSecurity(config: NextConfig): NextConfig {
return {
...config,
async headers() {
const existing = (await config.headers?.()) ?? [];
return [
...existing,
{
source: '/(.*)',
headers: [{ key: 'X-Frame-Options', value: 'DENY' }],
},
];
},
};
}
function withPerformance(config: NextConfig): NextConfig {
return {
...config,
images: {
...config.images,
formats: ['image/avif', 'image/webp'],
},
};
}
const baseConfig: NextConfig = {
experimental: { ppr: 'incremental' },
};
export default withSecurity(withPerformance(baseConfig));
3. Environment-based next.config.ts Branching
// next.config.ts
import type { NextConfig } from 'next';
const isDev = process.env.NODE_ENV === 'development';
const isProd = process.env.NODE_ENV === 'production';
const nextConfig: NextConfig = {
// Standalone build only in production
...(isProd && { output: 'standalone' }),
// Enable source maps only in development
...(isDev && { productionBrowserSourceMaps: true }),
experimental: {
ppr: isProd ? true : 'incremental',
reactCompiler: isProd,
},
};
export default nextConfig;
4. Turbopack-specific Configuration
const nextConfig: NextConfig = {
// Turbopack config (applied when running next dev --turbopack)
turbopack: {
rules: {
// Import SVG as React components
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
resolveAlias: {
// Module aliases
underscore: 'lodash',
},
},
};
5. Build Performance Checks
# Measure build time
time npm run build
# Analyze bundle size
ANALYZE=true npm run build
# Type check only (without building)
npx tsc --noEmit
# Run lint only
npm run lint