// playbook

How to make a Vite or CRA app production-ready

A default Vite or Create React App scaffold is a great starting point for a demo. It is not a starting point for a production app. Here is the concrete playbook to close the gap.

Published 2026-05-06 12 min read

The default Vite or CRA scaffold gives you: a build tool, a dev server, a sample component, a sample test, and nothing else. That is fine for showing a friend. To ship to paying users it needs another seven things. None of them are exotic.

This playbook assumes Vite + React + TypeScript. If you are on CRA: most of the steps are identical, but consider migrating to Vite first. CRA is officially archived and accumulating CVEs in react-scripts. The migration is a half-day project.

Starting point

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install

You now have src/App.tsx, vite.config.ts, and a working npm run dev. Run it once to confirm. Then we make it production-ready.

Step 1 — Strict TypeScript

Open tsconfig.json and ensure these are set:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": true
  }
}

strict: true is the floor. The other four catch entire classes of subtle bugs. noUncheckedIndexedAccess in particular saves you from runtime "cannot read property of undefined" errors that the AI happily generated.

Add a typecheck script:

// package.json
"scripts": {
  "typecheck": "tsc --noEmit"
}

Step 2 — ESLint and Prettier

Vite's React-TS template ships ESLint. Verify it works: npm run lint. If it does not exist or is missing rules, install:

npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin \
  eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-import \
  prettier eslint-config-prettier

Minimal .eslintrc.cjs:

module.exports = {
  root: true,
  env: { browser: true, es2022: true },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
    'prettier'
  ],
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint', 'react-refresh'],
  rules: {
    'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
  },
}

Add to package.json:

"scripts": {
  "lint": "eslint src --ext ts,tsx",
  "format": "prettier --write src"
}

Step 3 — Real tests, not the default placeholder

Install Vitest:

npm install -D vitest @testing-library/react @testing-library/jest-dom @vitest/ui jsdom

Add to vite.config.ts:

/// <reference types="vitest" />
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'],
  },
});

Create src/test-setup.ts:

import '@testing-library/jest-dom';

Add scripts:

"scripts": {
  "test": "vitest run",
  "test:watch": "vitest",
  "test:ui": "vitest --ui"
}

Now write at least three real tests: one for your auth flow, one for your payment / checkout flow, one for the core happy path of your product. These three save you from the worst regressions. Skip aiming for 100% coverage; aim for "the things that matter cannot break silently."

Step 4 — Continuous integration

Drop in .github/workflows/ci.yml:

name: CI
on:
  pull_request:
  push:
    branches: [main]

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test
      - run: npm run build
      - run: npm audit --audit-level=high

Now every PR and every push to main runs lint, typecheck, tests, build, and an audit for known CVEs. The day you accidentally introduce a high-severity vulnerability or a type error, GitHub stops you from merging it.

Step 5 — Error tracking with Sentry

Sign up for Sentry's free tier, create a React project, get the DSN.

npm install @sentry/react

In src/main.tsx:

import * as Sentry from '@sentry/react';

if (import.meta.env.PROD) {
  Sentry.init({
    dsn: import.meta.env.VITE_SENTRY_DSN,
    environment: import.meta.env.MODE,
    tracesSampleRate: 0.1,
    replaysSessionSampleRate: 0,    // session replay is paid; skip for now
    replaysOnErrorSampleRate: 1.0,
  });
}

Wrap your root with Sentry.ErrorBoundary for component errors, and you are done. Every uncaught exception now lands in Sentry with a stack trace and breadcrumbs.

Step 6 — Environment variables, properly

Vite exposes only env vars prefixed VITE_ to the client bundle. Use this distinction.

If you put your OpenAI key in VITE_OPENAI_KEY, congratulations — every visitor of your site can read it from the JS bundle. Move it to a backend route.

Commit .env.example, never .env:

# .gitignore
.env
.env.local
.env.production
.env.*
!.env.example

Step 7 — Deployment with security headers

If you deploy to Vercel, add vercel.json:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Frame-Options", "value": "SAMEORIGIN" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" },
        { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload" }
      ]
    }
  ]
}

If you deploy to Netlify, the same in _headers or netlify.toml. If you self-host with nginx, add the headers there.

Test what you got at securityheaders.com. Aim for an A.

Step 8 — Bundle size sanity

Run a build:

npm run build

Look at the output. If your main chunk is over 300 KB gzipped, something is wrong — usually you imported a giant library you barely use. Common culprits: moment (use date-fns or native Intl), lodash (use individual imports or built-ins), full icon libraries instead of tree-shaken imports, charting libraries you only use one chart from.

Install the visualizer to actually see what is in your bundle:

npm install -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';

plugins: [react(), visualizer({ open: true, gzipSize: true })],

Run npm run build and the visualization opens in your browser. The expensive culprits become obvious.

Step 9 — Health endpoint and uptime monitoring

If your Vite app is purely static, the host's HTTP probe is fine — point UptimeRobot at https://yourapp.com. If you have an API, expose /health returning JSON:

// example: a small Express backend
app.get('/health', (_, res) => res.json({ ok: true, ts: Date.now() }));

Sign up for a free uptime monitor (UptimeRobot, Better Stack) and set a 1-minute check. You are now notified within 60 seconds of any outage.


The total time investment

Steps 1–4 (typecheck, lint, tests, CI): half a day. Steps 5–7 (Sentry, env hygiene, security headers): two hours. Steps 8–9 (bundle audit, uptime): another hour.

You will spend roughly one weekend doing this. The day you have a paying customer, you will be glad you did. The day you have a hundred, even more.

What this does not cover

This playbook covers the frontend hardening. If your app has a backend, you also need: rate limiting, input validation (use zod), CORS allowlist, structured logging with secret redaction, database backups, and a migration strategy. The common security gaps post covers most of those.

How to know what is missing in your repo

Run a free CodeClanker scan. We will tell you which of these nine items are missing in your specific codebase, plus check for committed secrets, vulnerable dependencies via OSV.dev, and license issues. The output is the prioritized fix list, not a generic checklist.

Skip the checklist — get a personalized one

Free 60-second scan. We name every gap with the file or dependency that proves it.

Run a free scan →