design-patterns

ABOUTME: Architectural patterns skill for TypeScript ecommerce

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "design-patterns" with this command: npx skills add lorenzogirardi/ai-ecom-demo/lorenzogirardi-ai-ecom-demo-design-patterns

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; }

  1. 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); }, });

  1. 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, };

  1. 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? });

  1. 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

  1. 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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

spec-driven-dev

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

typescript

No summary provided by upstream source.

Repository SourceNeeds Review
General

trivy

No summary provided by upstream source.

Repository SourceNeeds Review
General

scm

No summary provided by upstream source.

Repository SourceNeeds Review