design-patterns

SKILL.md

ABOUTME: Architectural patterns skill for TypeScript ecommerce

ABOUTME: Covers DI, error handling, testing, and common anti-patterns

Design Patterns (Ecommerce)

Architectural patterns for the TypeScript/Node.js ecommerce stack.

Quick Reference

Pattern Frontend (Next.js) Backend (Fastify)
DI React Context Constructor injection
Errors Error boundaries Fastify error handler
Config env.local dotenv + config module
State React Query In-memory + Redis
Testing Testing Library Testcontainers

1. Dependency Injection

Backend (Fastify)

// Define interface
interface UserRepository {
  findById(id: string): Promise<User | null>;
  create(data: CreateUserDto): Promise<User>;
}

// Constructor injection
class UserService {
  constructor(
    private readonly userRepo: UserRepository,
    private readonly logger: Logger
  ) {}

  async getUser(id: string): Promise<User> {
    this.logger.info({ id }, 'Getting user');
    const user = await this.userRepo.findById(id);
    if (!user) throw new NotFoundError(`User ${id} not found`);
    return user;
  }
}

// Wire up in app
const userRepo = new PrismaUserRepository(prisma);
const userService = new UserService(userRepo, logger);

Frontend (React Context)

// Context for dependency injection
const CartContext = createContext<CartContextValue | null>(null);

export function CartProvider({ children }: { children: React.ReactNode }) {
  const [items, setItems] = useState<CartItem[]>([]);

  const addItem = useCallback((product: Product, quantity: number) => {
    setItems((prev) => [...prev, { product, quantity }]);
  }, []);

  return (
    <CartContext.Provider value={{ items, addItem }}>
      {children}
    </CartContext.Provider>
  );
}

export function useCart() {
  const context = useContext(CartContext);
  if (!context) throw new Error('useCart must be used within CartProvider');
  return context;
}

2. Error Handling

Backend (Fastify)

// Custom error classes
class AppError extends Error {
  constructor(
    message: string,
    public statusCode: number = 500,
    public code: string = 'INTERNAL_ERROR'
  ) {
    super(message);
    this.name = 'AppError';
  }
}

class NotFoundError extends AppError {
  constructor(message: string) {
    super(message, 404, 'NOT_FOUND');
  }
}

class ValidationError extends AppError {
  constructor(message: string) {
    super(message, 400, 'VALIDATION_ERROR');
  }
}

// Error handler middleware
function errorHandler(error: Error, request: FastifyRequest, reply: FastifyReply) {
  if (error instanceof AppError) {
    return reply.status(error.statusCode).send({
      statusCode: error.statusCode,
      error: error.code,
      message: error.message,
    });
  }

  // Log unexpected errors
  request.log.error(error);
  return reply.status(500).send({
    statusCode: 500,
    error: 'INTERNAL_ERROR',
    message: 'An unexpected error occurred',
  });
}

Frontend (Error Boundaries)

// Error boundary for React
class ErrorBoundary extends React.Component<Props, State> {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error('Error caught:', error, info);
  }

  render() {
    if (this.state.hasError) {
      return <ErrorFallback error={this.state.error} />;
    }
    return this.props.children;
  }
}

// React Query error handling
const { data, error } = useQuery({
  queryKey: ['products'],
  queryFn: fetchProducts,
  retry: 3,
  onError: (error) => {
    toast.error(error.message);
  },
});

3. Configuration

Backend

// config/index.ts
import { z } from 'zod';

const ConfigSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  PORT: z.coerce.number().default(4000),
  DATABASE_URL: z.string(),
  REDIS_HOST: z.string().default('localhost'),
  REDIS_PORT: z.coerce.number().default(6379),
  JWT_SECRET: z.string(),
});

export type Config = z.infer<typeof ConfigSchema>;

export function loadConfig(): Config {
  const result = ConfigSchema.safeParse(process.env);
  if (!result.success) {
    console.error('Invalid configuration:', result.error.format());
    process.exit(1);
  }
  return result.data;
}

export const config = loadConfig();

Frontend

// next.config.js handles env
// Access via process.env.NEXT_PUBLIC_*

// For runtime config
const config = {
  apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',
  environment: process.env.NODE_ENV,
};

4. Testing Patterns

Prefer Real Over Mocks

// GOOD: Real database with Testcontainers
describe('UserRepository', () => {
  let container: StartedPostgreSqlContainer;
  let prisma: PrismaClient;

  beforeAll(async () => {
    container = await new PostgreSqlContainer().start();
    prisma = new PrismaClient({
      datasources: { db: { url: container.getConnectionUri() } },
    });
  });

  afterAll(async () => {
    await prisma.$disconnect();
    await container.stop();
  });

  it('creates a user', async () => {
    const repo = new PrismaUserRepository(prisma);
    const user = await repo.create({ email: 'test@example.com' });
    expect(user.email).toBe('test@example.com');
  });
});

// BAD: Excessive mocking
it('creates a user', async () => {
  const mockPrisma = { user: { create: jest.fn().mockResolvedValue({ id: '1' }) } };
  // This tests mock behavior, not real behavior
});

Test Behavior, Not Implementation

// GOOD: Tests observable behavior
it('rejects invalid email', async () => {
  const response = await app.inject({
    method: 'POST',
    url: '/auth/register',
    payload: { email: 'invalid', password: 'secret123' },
  });
  expect(response.statusCode).toBe(400);
  expect(response.json()).toMatchObject({
    error: 'VALIDATION_ERROR',
  });
});

// BAD: Tests internal implementation
it('calls validateEmail function', async () => {
  const spy = jest.spyOn(validator, 'validateEmail');
  await register({ email: 'test@example.com' });
  expect(spy).toHaveBeenCalled(); // Who cares?
});

5. Common Anti-Patterns

TypeScript

Anti-Pattern Problem Solution
any type No type safety Use unknown + guards
// @ts-ignore Hidden bugs Fix the type issue
Optional chaining abuse Hides nulls Explicit null checks
as casting Runtime errors Type guards

React

Anti-Pattern Problem Solution
Prop drilling Coupling Context or state lib
useEffect for data Race conditions React Query
Inline styles No reuse Tailwind classes
Index as key Render bugs Stable IDs

Fastify

Anti-Pattern Problem Solution
Sync in handlers Blocks event loop Always async
Global state Race conditions Inject dependencies
No validation Security risk Zod schemas
Catching all errors Hides bugs Let Fastify handle

6. Naming Conventions

Files

# Components: PascalCase
src/components/ProductCard.tsx
src/components/CartSummary.tsx

# Hooks: camelCase with use prefix
src/hooks/useProducts.ts
src/hooks/useCart.ts

# Modules: kebab-case
src/modules/auth/auth.routes.ts
src/modules/catalog/catalog.service.ts

# Types: PascalCase
src/types/Product.ts
src/types/Order.ts

Variables

// Constants: SCREAMING_SNAKE_CASE
const MAX_RETRIES = 3;
const DEFAULT_PAGE_SIZE = 20;

// Functions: camelCase
function calculateTotal(items: CartItem[]): number {}
async function fetchProducts(categoryId?: string): Promise<Product[]> {}

// Classes: PascalCase
class OrderService {}
class ProductRepository {}

// Interfaces: PascalCase (no I prefix)
interface User {}
interface CartItem {}

Resources

See references/typescript-patterns.md for more detailed examples.

Weekly Installs
4
First Seen
3 days ago
Installed on
codex2
claude-code2
antigravity2
gemini-cli2
windsurf1
trae1