Development Environment Setup¶
Complete guide to setting up a modern React/Next.js development environment.
Prerequisites¶
Required Software¶
| Tool | Version | Purpose |
|---|---|---|
| Node.js | 18.17+ or 20.x | JavaScript runtime |
| npm/yarn/pnpm | Latest | Package manager |
| Git | 2.x+ | Version control |
| VSCode | Latest | Code editor (recommended) |
Installation¶
Node.js (via nvm - recommended):
# Install nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
# Install Node.js
nvm install 20
nvm use 20
nvm alias default 20
# Verify
node --version # v20.x.x
npm --version # 10.x.x
Alternative package managers:
Create New Next.js Project¶
Using Create Next App¶
# Interactive setup
npx create-next-app@latest my-project
# With specific options
npx create-next-app@latest my-project \
--typescript \
--tailwind \
--app \
--src-dir \
--import-alias "@/*"
cd my-project
Setup prompts:
✔ 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? Yes
✔ Would you like to use App Router? Yes
✔ Would you like to customize the default import alias? No
Project Structure¶
my-project/
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── globals.css
│ ├── components/
│ ├── lib/
│ └── types/
├── public/
├── .env.local
├── .eslintrc.json
├── .gitignore
├── next.config.js
├── package.json
├── tailwind.config.ts
└── tsconfig.json
Essential Dependencies¶
Core Libraries¶
# State Management
npm install zustand
npm install @tanstack/react-query
# Forms
npm install react-hook-form
npm install zod @hookform/resolvers
# UI Components
npm install @radix-ui/react-dialog
npm install @radix-ui/react-dropdown-menu
npm install class-variance-authority clsx tailwind-merge
# Date/Time
npm install date-fns
# Icons
npm install lucide-react
Development Dependencies¶
# Testing
npm install -D vitest @testing-library/react @testing-library/jest-dom
npm install -D @testing-library/user-event @vitejs/plugin-react
npm install -D @playwright/test
# Code Quality
npm install -D eslint-config-prettier prettier
npm install -D @typescript-eslint/eslint-plugin
npm install -D eslint-plugin-react-hooks
# Git Hooks
npm install -D husky lint-staged
# Type Checking
npm install -D @types/node @types/react @types/react-dom
Configuration Files¶
TypeScript Configuration¶
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"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": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Next.js Configuration¶
next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
// Enable experimental features
experimental: {
serverActions: {
bodySizeLimit: '2mb',
},
},
// Image optimization
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
},
],
formats: ['image/webp', 'image/avif'],
},
// Environment variables exposed to browser
env: {
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
},
// Build output
output: 'standalone', // For Docker
// Security headers
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
],
},
];
},
};
module.exports = nextConfig;
ESLint Configuration¶
.eslintrc.json:
{
"extends": [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"plugins": ["@typescript-eslint"],
"rules": {
"react/no-unescaped-entities": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"react-hooks/exhaustive-deps": "warn",
"no-console": ["warn", { "allow": ["warn", "error"] }],
"@typescript-eslint/no-explicit-any": "warn"
}
}
Prettier Configuration¶
.prettierrc:
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"tabWidth": 2,
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"]
}
.prettierignore:
Tailwind Configuration¶
tailwind.config.ts:
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
},
},
plugins: [],
};
export default config;
VSCode Setup¶
Recommended Extensions¶
Create .vscode/extensions.json:
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"ms-playwright.playwright",
"usernamehw.errorlens",
"streetsidesoftware.code-spell-checker",
"PKief.material-icon-theme",
"eamodio.gitlens"
]
}
Workspace Settings¶
Create .vscode/settings.json:
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
],
"files.associations": {
"*.css": "tailwindcss"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
Git Setup¶
Git Hooks with Husky¶
# Initialize Husky
npx husky init
# Create pre-commit hook
echo "npx lint-staged" > .husky/pre-commit
# Create commit-msg hook (optional)
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
Lint-Staged Configuration¶
package.json:
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml}": [
"prettier --write"
]
}
}
Commit Conventions (Optional)¶
commitlint.config.js:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore'],
],
},
};
Environment Variables¶
File Structure¶
.env.local # Local development (gitignored)
.env.development # Development defaults
.env.production # Production defaults
.env.example # Template for others
Example Configuration¶
.env.example:
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
# Authentication
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="generate-with-openssl-rand-base64-32"
# API Keys
NEXT_PUBLIC_API_URL="https://api.example.com"
API_SECRET_KEY="your-secret-key"
# Feature Flags
NEXT_PUBLIC_ENABLE_ANALYTICS="false"
.env.local (create this):
# Copy from .env.example and fill in real values
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
NEXTAUTH_SECRET="your-generated-secret"
Type-Safe Environment Variables¶
src/lib/env.ts:
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
NEXTAUTH_URL: z.string().url(),
NEXT_PUBLIC_API_URL: z.string().url(),
});
export const env = envSchema.parse({
DATABASE_URL: process.env.DATABASE_URL,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
});
Database Setup (Optional)¶
Prisma Setup¶
# Install Prisma
npm install prisma @prisma/client
npx prisma init
# This creates:
# - prisma/schema.prisma
# - .env with DATABASE_URL
prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Generate client:
Create Prisma client singleton:
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
Package.json Scripts¶
Recommended scripts:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"format": "prettier --write \"**/*.{ts,tsx,md,json}\"",
"format:check": "prettier --check \"**/*.{ts,tsx,md,json}\"",
"type-check": "tsc --noEmit",
"test": "vitest",
"test:watch": "vitest watch",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"db:push": "prisma db push",
"db:studio": "prisma studio",
"prepare": "husky install"
}
}
Testing Setup¶
Vitest Configuration¶
vitest.config.ts:
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./test/setup.ts'],
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
test/setup.ts:
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
afterEach(() => {
cleanup();
});
Playwright Configuration¶
playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Docker Setup (Optional)¶
Dockerfile¶
FROM node:20-alpine AS base
# Dependencies
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Builder
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Runner
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
docker-compose.yml¶
version: '3.8'
services:
app:
build: .
ports:
- '3000:3000'
environment:
- DATABASE_URL=postgresql://user:password@db:5432/mydb
depends_on:
- db
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- '5432:5432'
volumes:
postgres_data:
Quick Start Checklist¶
- Install Node.js (v18.17+ or v20.x)
- Install package manager (npm/yarn/pnpm)
- Create new Next.js project
- Install VSCode and extensions
- Configure TypeScript
- Set up ESLint and Prettier
- Configure Git hooks (Husky)
- Create .env files
- Set up database (if needed)
- Configure testing tools
- Run
npm run devand verify
Troubleshooting¶
Common Issues¶
Port 3000 already in use:
Module not found:
TypeScript errors: