For most of the last decade, starting a TypeScript project meant assembling six tools : npm, ts-node, ESLint, Prettier, Jest, ts-node-dev wiring them together with a config file each, and praying nothing hit a peer-dependency conflict by Friday. In 2026 you can do better. Bun handles the runtime, the package manager, and the test runner. Biome handles linting and formatting in one binary. Vitest covers the cases Bun's test runner doesn't. The whole toolchain fits in three dependencies and starts up in milliseconds.
This post sets up a project end-to-end the way I'd start one today, with the trade-offs of each choice spelled out so you can swap pieces if your team has constraints.
The shape of the stack
- Bun : runs TypeScript natively (no transpile step), installs packages 10-30× faster than npm, and includes a test runner and bundler.
- Biome : a Rust-based linter + formatter that replaces ESLint and Prettier with a single binary and one config file.
- Vitest : fast, Jest-compatible test runner with first-class TypeScript support. Use it when you need features Bun's runner doesn't have yet (browser-like environments, snapshot testing).
- tsc : still the source of truth for type checking. Bun and Biome don't replace it.
Project bootstrap
curl -fsSL https://bun.sh/install | bash # install Bun if needed
mkdir my-app && cd my-app
bun init -y # creates package.json, tsconfig.json, index.ts
bun add -d @biomejs/biome vitest @types/bun
bunx @biomejs/biome init
That's the entire setup. No npm install eslint prettier jest ts-node ..., no .eslintrc, no .prettierrc.
A modern tsconfig.json
Bun's default tsconfig.json is fine but worth tightening. Here's what I run with:
{
"compilerOptions": {
"target": "ESNext",
"module": "Preserve",
"moduleResolution": "bundler",
"lib": ["ESNext"],
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"noEmit": true,
"types": ["bun-types"]
},
"include": ["src/**/*", "tests/**/*"]
}
The four flags worth mentioning:
- noUncheckedIndexedAccess :
arr[0]is typed asT | undefined, notT. Catches a real class of bug at the cost of a few more ifs. - exactOptionalPropertyTypes : distinguishes "missing" from "explicitly undefined". Useful when wrapping APIs that treat them differently.
- verbatimModuleSyntax : forces explicit
import typefor type-only imports. Pairs well with bundlers that strip imports based on this signal. - noEmit : Bun does the running.
tsconly type-checks.
Biome: lint and format in one config
The default biome.json after biome init is reasonable. Tweak to taste:
{
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
"organizeImports": { "enabled": true },
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": { "noBannedTypes": "error" },
"suspicious": { "noExplicitAny": "warn" }
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"lineEnding": "lf"
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "all",
"semicolons": "always"
}
}
}
Run it from package.json scripts:
{
"scripts": {
"dev": "bun --watch src/index.ts",
"build": "bun build src/index.ts --outdir dist --target node",
"check": "biome check --write .",
"lint": "biome lint .",
"format": "biome format --write .",
"test": "bun test",
"test:ui": "vitest",
"type": "tsc --noEmit"
}
}
Bun's built-in test runner
// src/sum.ts
export const sum = (a: number, b: number) => a + b;
// tests/sum.test.ts
import { describe, expect, test } from 'bun:test';
import { sum } from '../src/sum';
describe('sum', () => {
test('adds two positive numbers', () => {
expect(sum(2, 3)).toBe(5);
});
test('handles negatives', () => {
expect(sum(-2, 3)).toBe(1);
});
});
bun test
# bun test v1.x.x
# 2 pass / 0 fail / ran in 21ms
21ms including process start. That's faster than Jest's banner takes to print. For unit tests of pure code, Bun is the right pick.
When to reach for Vitest instead
Bun's test runner is excellent but still missing a few things many real projects need: a polished JSDOM environment, browser mode via Playwright, and the deeper Jest-API surface. When any of those matter, Vitest is the call:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
coverage: { reporter: ['text', 'html'] },
},
});
Mix and match: keep bun test for the bulk of unit tests, run Vitest only for the suites that need a DOM or browser environment. Both can coexist.
CI: one command, no Node
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with: { bun-version: latest }
- run: bun install --frozen-lockfile
- run: bun run check
- run: bun run type
- run: bun test
No setup-node, no separate ESLint/Prettier steps. One install, three checks. CI runs in well under a minute on a typical small project.
When this stack isn't right
- You're shipping a library consumed by Node-only users : Bun runtime APIs (
Bun.file,Bun.serve) won't be available to your consumers. Avoid them and prefer Web standards. - Your team relies on a specific ESLint plugin Biome doesn't yet have a port for. Biome's rule coverage is excellent for the common cases but not 1:1 with the ESLint ecosystem.
- You ship to AWS Lambda's Node runtime : you'll still want to test on real Node before deploying. Build with Bun, test with both.
Why this matters
None of these tools are revolutionary in isolation. The win is the combination: fewer dependencies, fewer config files, faster install, faster test, faster CI. The hours you used to spend yak-shaving the toolchain go back into the actual project. After three months on this setup the difference is hard to overstate every other Node project I open feels like it boots up in molasses.
The best toolchain is the one you can hold entirely in your head. Bun + Biome + Vitest is small enough to do that.
Start a side-project with this stack this week. Once you feel the speed, going back to the old setup gets harder by the day.