MAG Editions
June 25, 20259 min read

React + TypeScript Production Setup: What Your Boilerplate Must Include in 2026

Every React TypeScript production boilerplate needs: Vite or Next.js, ESLint, Vitest, React Query, Zod, Tailwind. Full setup guide with folder structure and CI/CD.

React + TypeScript Production Setup: What Your Boilerplate Must Include in 2026

React + TypeScript Production Setup: What Your Boilerplate Must Include in 2026

Starting a React TypeScript project with create-react-app is no longer viable — it was officially deprecated in 2023. The current ecosystem offers faster, more capable alternatives, but also more choices. This guide covers every dependency, configuration, and structural decision a production-grade React TypeScript project needs in 2026 — with rationale for each choice.


Why Your Boilerplate Choices Matter

A poor initial setup costs days of refactoring when your team scales or your app grows. A solid boilerplate:

  • Enforces consistent code style (reducing review friction)
  • Catches bugs before they reach production (types + tests)
  • Keeps build times under 30 seconds locally
  • Deploys reliably from day one

The decisions you make in the first hour determine maintainability for the next two years.


Vite vs Next.js: The First Decision

| Feature | Vite 6 | Next.js 15 | |---------|--------|-----------| | Rendering | Client-side only (SPA) | SSR, SSG, ISR, RSC | | API routes | No (use a separate backend) | Yes (Route Handlers) | | Build speed (dev) | Instant HMR (under 50ms) | Fast, but slightly slower | | Build speed (prod) | Rollup-based, very fast | Webpack/Turbopack | | SEO | Requires prerendering setup | Built-in | | Deployment | Any static host | Vercel-optimized, but portable | | Complexity | Low | Medium | | Bundle size baseline | ~40 KB | ~90 KB |

Use Vite when: SPA, dashboard, internal tool, Electron app, Chrome extension.

Use Next.js when: Marketing site + app in one, blog, e-commerce, anything requiring server-side rendering for SEO.

Initialize a Vite Project

pnpm create vite my-app --template react-ts
cd my-app
pnpm install

Initialize a Next.js Project

pnpm create next-app my-app --typescript --tailwind --eslint --app --src-dir

TypeScript Configuration

Your tsconfig.json must enable strict mode from day one:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

noUncheckedIndexedAccess adds undefined to array index access types, preventing the most common runtime crashes in TypeScript apps. Enable it from the start.


Essential Dependencies

Core Dependencies Table

| Package | Version (2025) | Purpose | Required | |---------|---------------|---------|----------| | react | 19.x | UI library | Yes | | react-dom | 19.x | DOM renderer | Yes | | @tanstack/react-query | 5.x | Server state management | Yes | | zod | 3.x | Runtime schema validation | Yes | | react-hook-form | 7.x | Form state management | For forms | | zustand | 5.x | Client state management | If needed | | axios | 1.x | HTTP client | Optional (fetch works) |

Dev Dependencies Table

| Package | Purpose | Required | |---------|---------|----------| | typescript | TypeScript compiler | Yes | | vite / next | Build tool | Yes | | vitest | Unit/integration tests | Yes | | @testing-library/react | Component testing | Yes | | @testing-library/user-event | User interaction simulation | Yes | | eslint | Linter | Yes | | @typescript-eslint/eslint-plugin | TypeScript ESLint rules | Yes | | eslint-plugin-react-hooks | Hooks rules enforcement | Yes | | prettier | Code formatter | Yes | | tailwindcss | Utility-first CSS | Recommended | | husky | Git hooks | Recommended | | lint-staged | Run linters on staged files | Recommended |

Install everything at once:

pnpm add @tanstack/react-query zod react-hook-form zustand
pnpm add -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom
pnpm add -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
pnpm add -D eslint-plugin-react-hooks eslint-plugin-jsx-a11y
pnpm add -D prettier tailwindcss postcss autoprefixer
pnpm add -D husky lint-staged

Folder Structure

A feature-based folder structure scales better than a type-based one. Avoid the common mistake of organizing by components/, hooks/, utils/ at the top level — it breaks down at 20+ features.

src/
├── app/                    # Next.js App Router pages or Vite app entry
├── features/               # Feature modules (colocated)
│   ├── auth/
│   │   ├── components/     # Auth-specific components
│   │   ├── hooks/          # useAuth, useSession
│   │   ├── api/            # Auth API calls
│   │   ├── schemas/        # Zod schemas for auth data
│   │   └── index.ts        # Public API of the feature
│   ├── dashboard/
│   └── settings/
├── shared/                 # Truly shared across features
│   ├── components/         # Button, Input, Modal, etc.
│   ├── hooks/              # useDebounce, useLocalStorage
│   ├── lib/                # queryClient, axios instance, etc.
│   └── types/              # Global TypeScript types
├── styles/                 # Global CSS / Tailwind base
└── main.tsx                # Entry point

Rule: A file in features/auth/ can import from shared/. A file in shared/ must never import from features/. Features can only import other features through their index.ts public API.


Linting and Formatting

ESLint Configuration (flat config, ESLint 9)

// eslint.config.js
import tseslint from '@typescript-eslint/eslint-plugin'
import reactHooks from 'eslint-plugin-react-hooks'
import jsxA11y from 'eslint-plugin-jsx-a11y'

export default [
  {
    plugins: { '@typescript-eslint': tseslint, 'react-hooks': reactHooks, 'jsx-a11y': jsxA11y },
    rules: {
      ...tseslint.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      '@typescript-eslint/no-explicit-any': 'error',
      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
      'no-console': ['warn', { allow: ['warn', 'error'] }],
    }
  }
]

Prettier Configuration

{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 100,
  "tabWidth": 2
}

Set up Husky + lint-staged to run ESLint and Prettier on every commit:

pnpm dlx husky init
echo "pnpm lint-staged" > .husky/pre-commit
// package.json
"lint-staged": {
  "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
  "*.{json,md,css}": ["prettier --write"]
}

Testing Strategy: Vitest vs Jest

| Feature | Vitest 2.x | Jest 29.x | |---------|-----------|----------| | Speed | 2–10x faster | Slower (CommonJS transform) | | ES modules | Native | Requires config | | Config file | Unified with vite.config.ts | Separate jest.config.ts | | TypeScript | Out of the box | Requires ts-jest or babel | | Coverage | Built-in (v8 or istanbul) | Requires c8 or istanbul | | Watch mode | Instant | Slower | | Next.js support | Community | Official | | Ecosystem | Growing fast | Mature |

Use Vitest for Vite projects. Use Jest for Next.js if you need maximum ecosystem compatibility, otherwise Vitest works there too.

Vitest Setup

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/test/setup.ts'],
    coverage: {
      provider: 'v8',
      thresholds: { lines: 70, functions: 70, branches: 70 },
    },
  },
})
// src/test/setup.ts
import '@testing-library/jest-dom'

Testing Guidelines

  • Unit tests: Pure functions, hooks, Zod schemas
  • Component tests: With Testing Library — test behavior, not implementation
  • Integration tests: API + component together using MSW (Mock Service Worker)
  • E2E tests: Playwright for critical user flows (login, checkout)

Target: 70% line coverage on features/ and shared/. Do not test implementation details.


Data Fetching with React Query

React Query v5 (TanStack Query) is the standard for server state in React applications. It replaces useEffect + useState for any data that comes from an API.

Setup

// src/shared/lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60, // 1 minute
      retry: 1,
    },
  },
})
// src/main.tsx
import { QueryClientProvider } from '@tanstack/react-query'
import { queryClient } from '@/shared/lib/queryClient'

root.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
)

Query Pattern with Zod Validation

// src/features/users/api/getUser.ts
import { z } from 'zod'

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string(),
  createdAt: z.string().datetime(),
})

export type User = z.infer<typeof UserSchema>

export async function getUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) throw new Error('Failed to fetch user')
  return UserSchema.parse(await res.json())
}

CI/CD Pipeline

GitHub Actions (Minimal)

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
      - uses: actions/setup-node@v4
        with: { node-version: '22', cache: 'pnpm' }
      - run: pnpm install --frozen-lockfile
      - run: pnpm tsc --noEmit
      - run: pnpm lint
      - run: pnpm test --run --coverage
      - run: pnpm build

This pipeline runs in under 3 minutes on a standard GitHub Actions runner for a medium-sized project.

Deployment Targets

| Platform | Vite SPA | Next.js | Cost | |----------|----------|---------|------| | Vercel | Yes | Yes (native) | Free–$20/month | | Netlify | Yes | Yes | Free–$19/month | | Cloudflare Pages | Yes | Yes (adapter) | Free–$20/month | | Fly.io | Via Docker | Via Docker | $0–$variable | | AWS S3 + CloudFront | Yes | No | $1–$10/month |


Environment Variables and Type Safety

Never use process.env.VITE_API_URL directly. Validate env vars at startup with Zod:

// src/shared/lib/env.ts
import { z } from 'zod'

const EnvSchema = z.object({
  VITE_API_URL: z.string().url(),
  VITE_APP_ENV: z.enum(['development', 'staging', 'production']),
})

export const env = EnvSchema.parse(import.meta.env)

If a required env var is missing or malformed, the app crashes immediately with a clear error instead of silently failing at runtime.


Get a Production-Ready Boilerplate

Setting up all of this correctly takes 4–8 hours the first time. The MAG Editions React TypeScript Starter Kit ships with every configuration above pre-wired: Vite 6 + React 19, strict TypeScript, ESLint 9 flat config, Vitest + Testing Library, React Query v5, Zod, Tailwind v4, Husky, GitHub Actions, and a feature-based folder structure. Clone it and write your first feature in under 10 minutes.


Summary Checklist

  • [ ] Vite 6 or Next.js 15 (not CRA)
  • [ ] TypeScript strict mode enabled from day one
  • [ ] Feature-based folder structure (not type-based)
  • [ ] ESLint + Prettier + Husky pre-commit hook
  • [ ] Vitest for unit/component tests
  • [ ] React Query v5 for all server state
  • [ ] Zod for runtime validation of API responses and env vars
  • [ ] Tailwind CSS for styling
  • [ ] GitHub Actions CI running typecheck, lint, test, build
  • [ ] Coverage thresholds enforced (70% minimum)

Go further

React + TypeScript Production Starter Kit — 5 Templates Ready to Ship

Five production-ready React + TypeScript templates covering SaaS, landing pages, dashboards, and more.

View product