Code Standards

Coding standards and guidelines for contributing to ShipKit

Code Standards

This guide outlines the coding standards and guidelines for contributing to ShipKit.

TypeScript Guidelines

Type Safety

// ❌ Avoid implicit any
function process(data) {
  return data.id;
}

// ✅ Use explicit types
function process(data: { id: string }): string {
  return data.id;
}

// ❌ Avoid type assertions without checks
const user = data as User;

// ✅ Use type guards
function isUser(data: unknown): data is User {
  return (
    typeof data === "object" &&
    data !== null &&
    "id" in data &&
    "email" in data
  );
}

const user = isUser(data) ? data : null;

Type Definitions

// ❌ Avoid loose types
type Data = {
  [key: string]: any;
};

// ✅ Define specific types
interface User {
  id: string;
  email: string;
  name?: string;
  role: "admin" | "user";
  createdAt: Date;
}

// ✅ Use generics for reusable types
interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  hasMore: boolean;
}

// ✅ Use utility types effectively
type UserUpdate = Partial<Omit<User, "id">>;
type ReadonlyUser = Readonly<User>;

API Types

// src/types/api.ts
export interface ApiResponse<T> {
  data: T;
  meta?: {
    timestamp: string;
    version: string;
  };
}

export interface ApiError {
  code: string;
  message: string;
  details?: Record<string, unknown>;
}

// Usage
async function fetchUser(id: string): Promise<ApiResponse<User>> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    const error: ApiError = await response.json();
    throw new Error(error.message);
  }
  return response.json();
}

React Guidelines

Component Structure

// src/components/features/user/user-card.tsx
import { type ReactNode } from "react";
import { cn } from "@/lib/utils";

interface UserCardProps {
  user: User;
  actions?: ReactNode;
  className?: string;
}

export const UserCard = ({
  user,
  actions,
  className,
}: UserCardProps) => {
  return (
    <div
      className={cn(
        "rounded-lg border p-4",
        "bg-white shadow-sm",
        className
      )}
    >
      <div className="flex items-center gap-4">
        <Avatar
          src={user.avatar}
          alt={user.name}
          fallback={user.email[0]}
        />
        <div className="flex-1">
          <h3 className="font-semibold">{user.name}</h3>
          <p className="text-sm text-gray-500">{user.email}</p>
        </div>
        {actions && (
          <div className="flex items-center gap-2">
            {actions}
          </div>
        )}
      </div>
    </div>
  );
};

Hooks

// src/hooks/use-debounce.ts
import { useState, useEffect } from "react";

export function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Usage
const SearchInput = () => {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    // Search with debounced value
  }, [debouncedQuery]);
};

Testing Requirements

Unit Tests

// src/lib/utils.test.ts
import { describe, it, expect } from "vitest";
import { formatCurrency, validateEmail } from "./utils";

describe("formatCurrency", () => {
  it("formats USD correctly", () => {
    expect(formatCurrency(1234.56, "USD")).toBe("$1,234.56");
  });

  it("handles zero values", () => {
    expect(formatCurrency(0, "USD")).toBe("$0.00");
  });

  it("handles negative values", () => {
    expect(formatCurrency(-1234.56, "USD")).toBe("-$1,234.56");
  });
});

describe("validateEmail", () => {
  it.each([
    ["[email protected]", true],
    ["invalid-email", false],
    ["[email protected]", false],
    ["@example.com", false],
  ])("validates %s correctly", (email, expected) => {
    expect(validateEmail(email)).toBe(expected);
  });
});

Integration Tests

// src/features/auth/login.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./login-form";

describe("LoginForm", () => {
  it("submits form with valid data", async () => {
    const onSubmit = vi.fn();
    render(<LoginForm onSubmit={onSubmit} />);

    await userEvent.type(
      screen.getByLabelText(/email/i),
      "[email protected]"
    );
    await userEvent.type(
      screen.getByLabelText(/password/i),
      "password123"
    );
    await userEvent.click(screen.getByRole("button", { name: /sign in/i }));

    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({
        email: "[email protected]",
        password: "password123",
      });
    });
  });

  it("shows validation errors", async () => {
    render(<LoginForm onSubmit={vi.fn()} />);

    await userEvent.click(screen.getByRole("button", { name: /sign in/i }));

    expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
    expect(await screen.findByText(/password is required/i)).toBeInTheDocument();
  });
});

E2E Tests

// tests/e2e/auth.spec.ts
import { test, expect } from "@playwright/test";

test.describe("Authentication", () => {
  test("successful login flow", async ({ page }) => {
    await page.goto("/login");

    await page.fill('[name="email"]', "[email protected]");
    await page.fill('[name="password"]', "password123");
    await page.click('button[type="submit"]');

    await expect(page).toHaveURL("/dashboard");
    await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
  });

  test("invalid credentials", async ({ page }) => {
    await page.goto("/login");

    await page.fill('[name="email"]', "[email protected]");
    await page.fill('[name="password"]', "wrongpassword");
    await page.click('button[type="submit"]');

    await expect(page.locator(".error-message")).toContainText(
      /invalid credentials/i
    );
  });
});

Documentation

Code Comments

/**
 * Processes a payment transaction.
 *
 * @param amount - The amount to charge in cents
 * @param currency - The currency code (e.g., "USD")
 * @param customerId - The customer's unique identifier
 * @returns A promise that resolves to the transaction result
 * @throws {PaymentError} When the payment fails
 *
 * @example
 * ```ts
 * const result = await processPayment({
 *   amount: 2000, // $20.00
 *   currency: "USD",
 *   customerId: "cus_123"
 * });
 * ```
 */
async function processPayment({
  amount,
  currency,
  customerId,
}: PaymentParams): Promise<PaymentResult> {
  // Implementation
}

// Use comments to explain complex logic
function calculateDiscount(items: CartItem[]): number {
  // Apply bulk discount first
  const bulkDiscount = items.length >= 10 ? 0.1 : 0;

  // Then apply category-specific discounts
  const categoryDiscounts = items.reduce((acc, item) => {
    // Electronics get 5% off
    if (item.category === "electronics") {
      return acc + item.price * 0.05;
    }
    return acc;
  }, 0);

  return bulkDiscount + categoryDiscounts;
}

Component Documentation

// src/components/ui/button/button.tsx
import { cva, type VariantProps } from "class-variance-authority";

/**
 * Primary button component for user interactions.
 *
 * @example
 * ```tsx
 * <Button variant="primary" size="lg" onClick={handleClick}>
 *   Click me
 * </Button>
 * ```
 */
export const Button = ({
  children,
  variant = "primary",
  size = "md",
  ...props
}: ButtonProps) => {
  // Implementation
};

// Document prop types
interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  /**
   * The content to display inside the button
   */
  children: React.ReactNode;

  /**
   * The visual style variant of the button
   * @default "primary"
   */
  variant?: "primary" | "secondary" | "ghost";

  /**
   * The size of the button
   * @default "md"
   */
  size?: "sm" | "md" | "lg";
}

Best Practices

Code Quality

  1. Naming

    • Use descriptive names
    • Follow consistent conventions
    • Use PascalCase for components
    • Use camelCase for functions/variables
  2. File Organization

    • One component per file
    • Group related files
    • Use index files for exports
    • Keep files focused
  3. Error Handling

    • Use custom error classes
    • Provide meaningful messages
    • Handle edge cases
    • Log appropriately

Performance

  1. Optimization

    • Memoize expensive calculations
    • Use proper React hooks
    • Implement code splitting
    • Profile before optimizing
  2. Bundle Size

    • Monitor dependencies
    • Use tree-shaking
    • Lazy load components
    • Optimize imports

Security

  1. Data Handling

    • Validate inputs
    • Sanitize outputs
    • Use proper encoding
    • Implement CSRF protection
  2. Authentication

    • Use secure sessions
    • Implement proper auth flows
    • Handle tokens securely
    • Rate limit requests

Related Resources