javascript-excellence
JavaScript Excellence
This skill embodies DHH's coding philosophy adapted for JavaScript and TypeScript - emphasizing clarity, simplicity, and expressiveness.
Core Principles
- Clarity over Cleverness: Code should be obvious, not clever
- Simplicity over Complexity: The simplest solution is usually the best
- Expressiveness: Code should read like prose
- DRY (Don't Repeat Yourself): Eliminate duplication ruthlessly
- Convention over Configuration: Sensible defaults, customize when needed
- No Premature Abstraction: Wait until patterns emerge
Language Best Practices
Use Modern JavaScript Features
✅ Do use modern syntax:
// Destructuring
const { name, email } = user;
// Spread operators
const newUser = { ...user, verified: true };
// Optional chaining
const street = user?.address?.street;
// Nullish coalescing
const name = user.name ?? "Anonymous";
// Template literals
const greeting = `Hello, ${name}!`;
❌ Don't write old-style code:
// Don't
var name = user.name ? user.name : "Anonymous";
var newUser = Object.assign({}, user, { verified: true });
Prefer Const and Let
✅ Do use const by default:
const users = await getUsers();
const total = users.length;
❌ Don't use var:
var users = await getUsers(); // No!
Use Meaningful Names
✅ Do use descriptive names:
async function sendWelcomeEmail(user: User) {
const emailContent = buildWelcomeEmail(user);
await emailService.send(emailContent);
}
❌ Don't use cryptic abbreviations:
async function sndWlcmEml(u: User) { // What?
const ec = bldWlcmEml(u);
await es.snd(ec);
}
TypeScript Best Practices
Use Type Inference
✅ Do let TypeScript infer:
const users = await db.user.findMany(); // Type is inferred
const count = users.length; // Type is inferred
function double(n: number) {
return n * 2; // Return type inferred as number
}
❌ Don't over-specify:
const users: User[] = await db.user.findMany(); // Redundant
const count: number = users.length; // Redundant
function double(n: number): number { // Return type unnecessary
return n * 2;
}
Use Discriminated Unions for State
✅ Do model states explicitly:
type LoadingState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function render(state: LoadingState<User>) {
switch (state.status) {
case "idle":
return <div>Not started</div>;
case "loading":
return <div>Loading...</div>;
case "success":
return <div>{state.data.name}</div>; // data is available!
case "error":
return <div>Error: {state.error.message}</div>;
}
}
❌ Don't use boolean flags:
type LoadingState<T> = {
isLoading: boolean;
isError: boolean;
data?: T;
error?: Error;
};
// Can have invalid states like isLoading=true and isError=true
Keep Types Simple
✅ Do use simple types:
type User = {
id: string;
name: string;
email: string;
};
type CreateUserInput = Pick<User, "name" | "email">;
❌ Don't over-engineer types:
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
type RecursiveRequired<T> = {
[P in keyof T]-?: RecursiveRequired<T[P]>;
};
// Unless you really need this complexity!
Function Patterns
Keep Functions Small and Focused
✅ Do write focused functions:
async function createUser(data: CreateUserInput) {
const hashedPassword = await hashPassword(data.password);
const user = await db.user.create({
data: { ...data, password: hashedPassword }
});
await sendWelcomeEmail(user);
return user;
}
async function hashPassword(password: string) {
return bcrypt.hash(password, 10);
}
async function sendWelcomeEmail(user: User) {
// Email logic here
}
❌ Don't write giant functions:
async function createUser(data: CreateUserInput) {
// 200 lines of mixed concerns
// password hashing
// validation
// database access
// email sending
// logging
// etc.
}
Use Early Returns
✅ Do use guard clauses:
function processPayment(amount: number, user: User) {
if (amount <= 0) {
throw new Error("Invalid amount");
}
if (!user.paymentMethod) {
throw new Error("No payment method");
}
if (user.balance < amount) {
throw new Error("Insufficient balance");
}
// Happy path at the end
return chargePayment(user, amount);
}
❌ Don't nest deeply:
function processPayment(amount: number, user: User) {
if (amount > 0) {
if (user.paymentMethod) {
if (user.balance >= amount) {
return chargePayment(user, amount);
} else {
throw new Error("Insufficient balance");
}
} else {
throw new Error("No payment method");
}
} else {
throw new Error("Invalid amount");
}
}
Use Async/Await Consistently
✅ Do use async/await:
async function loadUserData(userId: string) {
const user = await db.user.findUnique({ where: { id: userId } });
const posts = await db.post.findMany({ where: { userId } });
return { user, posts };
}
❌ Don't mix promises and async/await:
async function loadUserData(userId: string) {
const user = await db.user.findUnique({ where: { id: userId } });
return db.post.findMany({ where: { userId } }).then(posts => {
return { user, posts };
});
}
Object and Array Patterns
Use Destructuring
✅ Do destructure:
function createPost({ title, content, authorId }: CreatePostInput) {
return db.post.create({
data: { title, content, authorId }
});
}
const { name, email } = user;
const [first, second, ...rest] = items;
Use Array Methods
✅ Do use functional array methods:
const activeUsers = users.filter(u => u.active);
const userNames = users.map(u => u.name);
const totalScore = scores.reduce((sum, score) => sum + score, 0);
const hasAdmin = users.some(u => u.role === "admin");
❌ Don't use for loops unnecessarily:
const activeUsers = [];
for (let i = 0; i < users.length; i++) {
if (users[i].active) {
activeUsers.push(users[i]);
}
}
Use Object Methods
✅ Do use Object methods:
const keys = Object.keys(user);
const values = Object.values(user);
const entries = Object.entries(user);
const merged = Object.assign({}, defaults, overrides);
// Or better:
const merged = { ...defaults, ...overrides };
Error Handling
Use Try-Catch for Async
✅ Do handle errors:
async function getUser(id: string) {
try {
return await db.user.findUnique({ where: { id } });
} catch (error) {
logger.error("Failed to fetch user", { error, id });
throw new Error("User not found");
}
}
Create Custom Errors
✅ Do use custom error classes:
class NotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = "NotFoundError";
}
}
class ValidationError extends Error {
constructor(message: string, public fields: string[]) {
super(message);
this.name = "ValidationError";
}
}
// Usage
if (!user) {
throw new NotFoundError("User not found");
}
Code Organization
Group Related Code
✅ Do organize by feature:
src/
features/
auth/
login.ts
signup.ts
session.ts
posts/
create.ts
list.ts
edit.ts
❌ Don't organize by type:
src/
controllers/
authController.ts
postController.ts
services/
authService.ts
postService.ts
models/
user.ts
post.ts
Extract Reusable Logic
✅ Do extract when patterns emerge:
// After seeing this pattern 3+ times
function requireAuth<T>(
handler: (userId: string, input: T) => Promise<Response>
) {
return async (input: T) => {
const userId = await getUserId();
if (!userId) {
throw redirect("/login");
}
return handler(userId, input);
};
}
// Use it
export const loader = requireAuth(async (userId, { params }) => {
// userId is guaranteed to exist
});
❌ Don't abstract too early:
// Don't create "BaseController" after one use
class BaseController {
// Generic abstraction nobody asked for
}
Comments and Documentation
Write Self-Documenting Code
✅ Do write clear code:
function calculateTotalPrice(items: CartItem[]) {
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const tax = subtotal * TAX_RATE;
return subtotal + tax;
}
❌ Don't comment obvious code:
// Calculate total price
function calc(items: CartItem[]) {
// Sum all items
const s = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
// Calculate tax
const t = s * TAX_RATE;
// Return total
return s + t;
}
Comment the "Why", Not the "What"
✅ Do explain reasoning:
// We disable scroll lock on mobile because iOS Safari has issues
// with position:fixed when the keyboard is open. See issue #123
if (isMobile) {
disableScrollLock();
}
❌ Don't state the obvious:
// Check if mobile
if (isMobile) {
// Disable scroll lock
disableScrollLock();
}
Testing Patterns
Write Readable Tests
✅ Do write clear tests:
test("creates user with hashed password", async () => {
const input = { email: "test@example.com", password: "secret123" };
const user = await createUser(input);
expect(user.email).toBe(input.email);
expect(user.password).not.toBe(input.password);
expect(await verifyPassword(input.password, user.password)).toBe(true);
});
Test Behavior, Not Implementation
✅ Do test outcomes:
test("returns 404 when post not found", async () => {
const response = await app.request("/posts/invalid-id");
expect(response.status).toBe(404);
});
❌ Don't test internals:
test("calls database with correct query", async () => {
const spy = vi.spyOn(db, "query");
await getPost("123");
expect(spy).toHaveBeenCalledWith({ id: "123" });
});
Anti-Patterns to Avoid
1. Callback Hell
❌ Don't nest callbacks:
getUser(userId, (error, user) => {
if (error) return handleError(error);
getPosts(user.id, (error, posts) => {
if (error) return handleError(error);
// ...more nesting
});
});
✅ Do use async/await:
const user = await getUser(userId);
const posts = await getPosts(user.id);
2. Mutation Everywhere
❌ Don't mutate unnecessarily:
function addItem(cart: Cart, item: Item) {
cart.items.push(item); // Mutates input
return cart;
}
✅ Do prefer immutability:
function addItem(cart: Cart, item: Item) {
return {
...cart,
items: [...cart.items, item]
};
}
3. God Objects
❌ Don't create giant classes:
class UserManager {
createUser() {}
updateUser() {}
deleteUser() {}
authenticateUser() {}
sendEmail() {}
generateReport() {}
// 50 more methods...
}
✅ Do use focused modules:
// user.ts
export function createUser() {}
export function updateUser() {}
// auth.ts
export function authenticate() {}
// email.ts
export function sendEmail() {}
Remember
DHH's philosophy in JavaScript:
- Write code that reads naturally
- Prefer simplicity over cleverness
- Eliminate duplication
- Keep functions small and focused
- Use the language's features effectively
- Don't abstract too early
- Make invalid states unrepresentable
- Test behavior, not implementation
Your code should be a joy to read and maintain.