Jest has been the default JavaScript testing framework for years. I used it without thinking. Then I migrated my AI SaaS to Vitest and the difference was immediate.
Here's what actually changed.
The Setup
My project: a Next.js 15 app with Claude API integration, Drizzle ORM, and a test suite of ~140 unit and integration tests. The pain point: Jest was taking 28-34 seconds for a full suite run in CI. Not catastrophic, but noticeable enough that I'd started skipping test runs locally.
Why Vitest
Vitest is built on Vite. It uses native ESM, shares your vite.config.ts setup, and runs tests in parallel by default. The headline claim is "up to 10x faster" than Jest. That's marketing copy — but directionally it's true.
The real advantage isn't raw speed. It's configuration.
Jest in a TypeScript + ESM + path-alias project requires:
-
ts-jestorbabel-jest -
moduleNameMapperfor every path alias - Custom
transformconfig -
extensionsToTreatAsEsmworkaround for ESM packages
Vitest requires: nothing. It reads your tsconfig.json and vite.config.ts and works.
Migration
The migration took about 45 minutes for a 140-test suite.
1. Install Vitest:
npm uninstall jest @types/jest ts-jest
npm install -D vitest @vitest/coverage-v8 @vitest/ui
2. Update vite.config.ts:
import { defineConfig } from 'vite'
import { resolve } from 'path'
export default defineConfig({
test: {
globals: true,
environment: 'node',
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
exclude: ['node_modules/', 'src/test/'],
},
},
resolve: {
alias: {
'@': resolve(__dirname, './src'),
'@db': resolve(__dirname, './src/db'),
},
},
})
3. Update test imports:
jest.fn() → vi.fn()
jest.spyOn() → vi.spyOn()
jest.mock() → vi.mock()
jest.clearAllMocks() → vi.clearAllMocks()
That's the full diff for 90% of test files. Global types (describe, it, expect) work identically.
4. Update package.json:
{"scripts":{"test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","test:ui":"vitest --ui"}}
Speed Results
| Run type | Jest | Vitest | Delta |
|---|---|---|---|
| Full suite (cold) | 31.4s | 8.2s | -74% |
| Full suite (warm) | 28.1s | 5.9s | -79% |
| Single file | 4.2s | 0.8s | -81% |
| Watch mode re-run | 3.1s | 0.4s | -87% |
CI went from ~34s to ~9s. That's meaningful — it means test feedback doesn't outlast a context switch.
One Gotcha: Dynamic Imports
If you use vi.mock() with dynamic imports, the hoisting behavior differs from Jest. Jest hoists jest.mock() calls to the top of the file automatically. Vitest does the same, but only when you're using vi.mock() with a factory function.
// This works in both
vi.mock('@/lib/stripe', () => ({
stripe: { checkout: { sessions: { create: vi.fn() } } }
}))
// This can fail if the import runs before vi.mock registers
import { processPayment } from '@/lib/payments'
Fix: use vi.hoisted() for setup that must run before imports:
const mockStripe = vi.hoisted(() => ({
checkout: { sessions: { create: vi.fn() } }
}))
vi.mock('@/lib/stripe', () => ({ stripe: mockStripe }))
Features Jest Doesn't Have
In-source testing — write tests directly in your source file, Vitest strips them in production builds:
// src/lib/price.ts
export function formatPrice(cents: number): string {
return `$${(cents / 100).toFixed(2)}`
}
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest
it('formats cents to dollars', () => {
expect(formatPrice(4900)).toBe('$49.00')
})
}
Vitest UI — a browser-based test runner with real-time pass/fail visualization. Run vitest --ui and open localhost:51204. Useful for debugging flaky tests.
Type checking in tests — vitest --typecheck runs TypeScript type checking as part of your test suite. Catches type errors that pass at runtime.
Should You Migrate?
If you're starting a new project: yes, use Vitest from day one.
If you're migrating an existing project: it's worth it if your suite is >50 tests and CI time matters. Budget 1-3 hours for the migration depending on how many Jest-specific patterns you're using.
The one case to stay on Jest: if you have a large library that explicitly targets Jest compatibility, or if you're in a monorepo where other packages must use Jest.
What I'm Using This In
The AI SaaS Starter Kit I maintain includes Vitest as the default test setup — pre-configured with coverage, path aliases, and test utilities for mocking Claude API calls in unit tests.
If you're building an AI product and want the full stack pre-wired — Next.js 15, Claude API, Stripe, Supabase, Drizzle, Vitest — check out whoffagents.com.
- AI SaaS Starter Kit — $99 one-time — includes full Vitest setup and example tests for API routes, webhooks, and agent workflows
- Ship Fast Skill Pack — $49 — includes Claude Code skills for TDD with Vitest and automated test generation