TypeScript/JavaScript project workflow guidelines using Bun package manager. Triggers on .ts, .tsx, bun, package.json, TypeScript...
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
Guidelines for working with TypeScript and JavaScript projects using Bun as the primary package manager with modern tooling and best practices.
| Task | Tool | Command |
|---|---|---|
| Lint + Format | Biome | bun run biome check --apply . |
| Type check | tsc | bun run tsc --noEmit |
| Dead code | ts-prune | bun run ts-prune |
| Test | Bun test | bun test |
| Coverage | c8 | bun run c8 bun test |
You MUST use Bun commands for all package and runtime operations in Bun projects:
# Package management
bun install # Install dependencies from package.json
bun add <package> # Add production dependency
bun add --dev <package> # Add development dependency
bun remove <package> # Remove dependency
# Running code and scripts
bun run <script> # Run script defined in package.json
bun <file.ts> # Run TypeScript/JavaScript directly
bun run build # Run build script
# Testing
bun test # Run tests with Bun's native test runner
# Package info
bun list # List installed packages
bun outdated # Check for updates
Benefits of Bun:
Default for Bun projects and modern TypeScript:
// Import named exports
import { UserService } from './services/user-service';
import { type User } from './types';
// Import default exports
import express from 'express';
// Import with alias
import * as helpers from './utils/helpers';
// Export named
export function getUserById(id: string): Promise<User> {
// ...
}
// Export default
export default UserService;
// Re-export
export { type User } from './types';
export { UserService } from './services/user-service';
Use only when necessary for legacy compatibility:
// Require imports
const { UserService } = require('./services/user-service');
const express = require('express');
// Module exports
module.exports = UserService;
module.exports = { UserService, UserRepository };
In package.json, specify module type:
{
"type": "module",
"name": "my-app",
"version": "1.0.0"
}
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"allowJs": false,
"outDir": "./dist",
"rootDir": "./src",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@services/*": ["src/services/*"],
"@models/*": ["src/models/*"]
},
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
Biome is the RECOMMENDED all-in-one tool for linting and formatting. It replaces ESLint and Prettier with faster performance and unified configuration.
Installation:
bun add --dev @biomejs/biome
Configuration (biome.json):
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"organizeImports": {
"enabled": true
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noUselessSwitchCase": "error"
},
"style": {
"noNonNullAssertion": "warn"
}
}
},
"javascript": {
"formatter": {
"semicolons": "always",
"quoteStyle": "single",
"trailingCommas": "es5"
}
}
}
Usage:
# Check and fix all issues
bun run biome check --apply .
# Format only
bun run biome format --write .
# Lint only
bun run biome lint .
# CI mode (check without fixing)
bun run biome check .
If a project uses ESLint/Prettier, migration to Biome is RECOMMENDED. For legacy support:
# ESLint
bun add --dev eslint
bun run eslint src/ --fix
# Prettier
bun add --dev prettier
bun run prettier --write src/
UserProfile.tsx, LoginForm.tsxformatDate.ts, apiClient.tsUser.ts, ApiResponse.tsAPI_ENDPOINTS.ts, CONFIG.ts.test.ts or .spec.ts suffix - user.service.test.ts// Classes/Types/Interfaces/Enums: PascalCase
class UserService { /* ... */ }
interface UserRepository { /* ... */ }
enum UserRole { Admin = 'ADMIN', User = 'USER' }
// Methods, properties, variables, functions: camelCase
getUserById(id: string): Promise<User>
const userData = {};
// Private members: camelCase with leading underscore
private _cache: Map<string, User> = new Map();
// Constants: UPPER_SNAKE_CASE
const MAX_RETRIES = 3;
const API_BASE_URL = 'https://api.example.com';
// React hooks: camelCase with use prefix
function useUserData(userId: string) { /* ... */ }
any - use unknown and type narrowing if neededany - enable noImplicitAny in tsconfig.json// Function parameters and return types
function processUser(user: User): Promise<ProcessedUser> {
// ...
}
// Arrow functions
const formatName = (first: string, last: string): string => {
return `${first} ${last}`;
};
// Complex types
type ApiResponse<T> = {
status: number;
data: T;
error?: string;
};
interface RequestHandler {
handle(request: Request): Promise<Response>;
}
// Generic functions
function getById<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find((item) => item.id === id);
}
// Generic classes
class Repository<T> {
async getById(id: string): Promise<T | null> {
// ...
}
}
// Generic types
type Result<T, E = Error> = { success: true; data: T } | { success: false; error: E };
Use Zod for runtime validation with type inference:
import { z } from 'zod';
// Schema definition
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
// Type inference from schema
type User = z.infer<typeof UserSchema>;
// Runtime validation
function createUser(data: unknown): User {
return UserSchema.parse(data);
}
// Safe parsing with error handling
const result = UserSchema.safeParse(data);
if (!result.success) {
console.error(result.error.format());
}
project/
āāā src/
ā āāā main.ts # Entry point
ā āāā types/ # Type definitions
ā āāā services/ # Business logic
ā āāā repositories/ # Data access layer
ā āāā models/ # Data models
ā āāā handlers/ # Request/event handlers
ā āāā middleware/ # Express/web middleware
ā āāā utils/ # Utility functions
ā āāā config/ # Configuration
āāā tests/ # Unit and integration tests
āāā dist/ # Compiled output (gitignored)
āāā package.json
āāā tsconfig.json
āāā biome.json
āāā bun.lock
// Absolute imports with path aliases
import { UserService } from '@services/user-service';
import type { User } from '@models/user';
// Relative imports within same feature
import { UserRepository } from '../repositories/user-repository';
import { validateUser } from '../utils/validators';
// Re-exports from index files
export { UserService, UserRepository } from './index';
// Define custom error classes
class AppError extends Error {
constructor(message: string, public code: string, public statusCode = 500) {
super(message);
this.name = 'AppError';
}
}
class ValidationError extends AppError {
constructor(message: string, public field: string) {
super(message, 'VALIDATION_ERROR', 400);
}
}
class NotFoundError extends AppError {
constructor(message: string) {
super(message, 'NOT_FOUND', 404);
}
}
// Result pattern for explicit error handling
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
async function safeFetchUser(id: string): Promise<Result<User, AppError>> {
try {
const user = await getUser(id);
return { ok: true, value: user };
} catch (error) {
return { ok: false, error: error instanceof AppError ? error : new AppError('Unknown', 'UNKNOWN') };
}
}
Use Zod for environment validation:
// env.ts
import { z } from 'zod';
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
API_KEY: z.string(),
});
export default EnvSchema.parse(process.env);
// Async/await with error handling
async function fetchUserData(id: string): Promise<User> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
} catch (error) {
console.error('Error fetching user:', error);
throw error;
}
}
// Concurrent operations with Promise.all
async function loadDashboardData(): Promise<DashboardData> {
const [users, products, stats] = await Promise.all([
fetchUsers(),
fetchProducts(),
fetchStats(),
]);
return { users, products, stats };
}
import { describe, it, expect, beforeEach } from 'bun:test';
import { UserService } from '@services/user-service';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
service = new UserService();
});
it('should fetch user by id', async () => {
const user = await service.getById('123');
expect(user).toBeDefined();
expect(user?.id).toBe('123');
});
});
See typescript-testing skill for comprehensive testing patterns.
Key Rules:
any - use unknown and type guards"strict": true)nextjs-workflowreact-workflowdatabase-workflowNote: For project-specific TypeScript patterns, check .claude/CLAUDE.md in the project directory.